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

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

Configuración

Si ya instalaste Google Cloud CLI y creaste una instancia de Redis, puedes omitir estos pasos.

  1. Instala la CLI de gcloud y realiza la inicialización:

    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 tu clúster GKE

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

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

      gcloud config set project [PROJECT_ID]

    2. Establece 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 creaste el clúster a través de gcloud, usa este comando para recuperar 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 GKE.
    2. CLUSTER_ZONE es la zona en la que se encuentra tu clúster.
    3. PROJECT_ID es el proyecto donde existen tu clúster y tus instancias de Redis.
  3. Si tu clúster es de la versión 1.8 o superior y tienes habilitados los alias de IP, omite este paso. Si tu clúster es de la versión 1.7 o inferior, o tu clúster de la versión 1.8 o superior no tiene habilitados los alias de IP, sigue estos pasos alternativos antes de intentar conectarse a tu instancia.

    1. Ejecuta estos comandos, reemplazando RESERVED_IP_RANGE con el rango 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 desconoces el rango de IP reservado de tu instancia, puedes averiguarlo a través de la consola (opciones avanzadas) o ingresando este 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 esta configuración habilitada, consulta la documentación de los alias de IP.

Aplicación de muestra

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

Clona el repositorio de tu lenguaje de programación deseado y navega a la carpeta que contiene el código de muestra:

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 muestra aumenta un contador de Redis cada vez que se accede al extremo /.

Go

En esta aplicación, se usa el cliente github.com/gomodule/redigo/redis. Para instalarla, ejecuta el siguiente comando:

go get github.com/gomodule/redigo/redis

// 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 el servlet Jetty 3.1.

Usa la biblioteca de 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

En esta aplicación, se 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 la entrega web y el paquete redis-py a fin de comunicarse con la instancia de Redis.

Flask==3.0.3
gunicorn==22.0.0
redis==5.0.1
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)

Compila la imagen de contenedor

Construye y envía la imagen del contenedor a Container Registry:

cp gke_deployment/Dockerfile .
export PROJECT_ID="$(gcloud config get-value project -q)"
docker build -t gcr.io/${PROJECT_ID}/visit-counter:v1 .
gcloud docker -- push gcr.io/${PROJECT_ID}/visit-counter:v1

Implementar la aplicación en Google Kubernetes Engine

Actualiza gke_deployment/visit-counter.yaml y reemplaza <PROJECT_ID> por el ID de tu proyecto de Google Cloud. Este archivo contiene la configuración para el servicio y la implementación.

Go

Para evitar la codificación IP de la instancia de Redis, puedes crear un redishost ConfigMap:

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

Verifica la configuración mediante el siguiente comando:

    kubectl get configmaps redishost -o yaml
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: visit-counter
  labels:
    app: visit-counter
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: visit-counter
    spec:
      containers:
      - name: visit-counter
        image: "gcr.io/<PROJECT_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

Java

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: visit-counter
  labels:
    app: visit-counter
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: visit-counter
    spec:
      containers:
      - name: visit-counter
        image: "gcr.io/<PROJECT_ID>/visit-counter:v1"
        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

Node.js

Para evitar la codificación IP de la instancia de Redis, puedes crear un redishost ConfigMap:

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

Verifica la configuración mediante el siguiente comando:

    kubectl get configmaps redishost -o yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: visit-counter
  labels:
    app: visit-counter
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: visit-counter
    spec:
      containers:
      - name: visit-counter
        image: "gcr.io/<PROJECT_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

Python

Para evitar la codificación IP de la instancia de Redis, puedes crear un redishost ConfigMap:

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

Verifica la configuración mediante el siguiente comando:

    kubectl get configmaps redishost -o yaml
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: visit-counter
  labels:
    app: visit-counter
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: visit-counter
    spec:
      containers:
      - name: visit-counter
        image: "gcr.io/<PROJECT-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

Determina la dirección [EXTERNAL-IP] para esta aplicación de muestra ejecutando este comando:

    kubectl get service visit-counter

Ve tu aplicación alojada en http://[EXTERNAL-IP] a través de tu navegador o envía una solicitud GET a través de cURL o de tu navegador:

    curl http://[EXTERNAL-IP]

Quitar la entrada de tablas de IP para la instancia de Redis

Si seguiste el paso tres de la sección de este instructivo denominado Prepara el clúster de GKE, ya instalaste el rango de IP reservado de tu instancia de Redis en las tablas de IP de tu instancia de GKE. Si deseas quitar esta entrada del rango de IP de Redis de las tablas de IP de tu instancia de GKE, ejecuta el siguiente comando desde el directorio k8s-custom-iptables/:

    ./uninstall.sh