Conectar-se a uma instância do Redis de um cluster do Google Kubernetes Engine

Só é possível se conectar à instância do Redis a partir dos clusters do Google Kubernetes Engine que usam a mesma rede autorizada que a instância do Redis.

Configuração

Essas etapas podem ser ignoradas se você já tiver instalado a Google Cloud CLI e criado uma instância do Redis.

  1. Instale a CLI gcloud e inicialize:

    gcloud init
    
  2. Siga o Guia de início rápido para criar uma instância do Redis. Anote a zona, o endereço IP e a porta da instância do Redis.

Como preparar seu cluster do GKE

  1. Se você não criou um cluster do GKE, crie um usando os seguintes comandos para a Google Cloud CLI:

    1. Designe o projeto para este aplicativo de amostra no gcloudgcloud.

      gcloud config set project PROJECT_ID

    2. Defina a variável de configuração da zona do Compute Engine em gcloud.

      gcloud config set compute/zone ZONE

    3. Criar um cluster do GKE chamado visitcount-cluster.

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

  2. Se você não criou o cluster usando gcloud, use o comando a seguir para recuperar as credenciais do cluster:

    gcloud container clusters get-credentials CLUSTER_NAME --zone CLUSTER_ZONE --project PROJECT_ID
    1. CLUSTER_NAME é o nome do cluster do GKE.
    2. CLUSTER_ZONE é a zona em que o cluster está.
    3. PROJECT_ID é o projeto em que estão o cluster e as instâncias do Redis.
  3. Se a versão do seu cluster for 1.8 ou superior e tiver aliases de IP ativados, pule esta etapa. Se a versão do seu cluster for 1.7 ou inferior, ou se o cluster da versão 1.8 ou superior não tiver aliases de IP ativados, siga estas etapas de solução alternativa antes de tentar se conectar à sua instância:

    1. Execute estes comandos, substituindo RESERVED_IP_RANGE pelo intervalo de IPs reservados da instância:

      git clone https://github.com/bowei/k8s-custom-iptables.git
      cd k8s-custom-iptables/
      TARGETS="RESERVED_IP_RANGE" ./install.sh
      cd ..
      
    2. Se você não souber o intervalo de IPs reservados da sua instância, descubra-o usando o console (opções avançadas) ou o seguinte comando:

      gcloud redis instances describe INSTANCE_ID --region=REGION
      

    Para mais informações sobre aliases de IP, incluindo como criar um cluster com essa configuração ativada, consulte a Documentação de aliases de IP.

Exemplo de app

Com este aplicativo de servidor HTTP de amostra, é estabelecida uma conexão com uma instância do Redis, a partir de um cluster do Google Kubernetes Engine.

Clone o repositório da linguagem de programação escolhida e navegue até a pasta que contém o código de amostra:

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

Este aplicativo de amostra incrementa um contador do Redis sempre que o endpoint / é acessado.

Go

Este aplicativo usa o cliente github.com/gomodule/redigo/redis. Instale-o executando o comando a seguir:

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

Exemplo de aplicativo:


// 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 aplicação é baseada em servlets do Jet 3.1.

Ele usa a biblioteca Jedis (em inglês):

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

A classe AppServletContextListener é usada para criar um pool de conexão Redis de longa duração:


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

A classe VisitCounterServlet é um servlet da Web que incrementa um contador do 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

Este aplicativo usa o módulo redis (em inglês).

{
  "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

Este aplicativo usa o Flask para veiculação na Web e o pacote redis-py (links em inglês) para se comunicar com a instância do 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)

Como construir a imagem do container

Crie e envie a imagem do contêiner para o 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

Como implantar um aplicativo no Google Kubernetes Engine

Para evitar a codificação do IP da instância do Redis, é possível criar um redishost ConfigMap:

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

Verifique a configuração usando o seguinte comando:

kubectl get configmaps redishost -o yaml

Atualize gke_deployment/visit-counter.yaml, substituindo <REPO_REGION>, <PROJECT_ID> e <REPO_ID> pelos valores da imagem do contêiner criada em Como criar a imagem do contêiner. Esse arquivo contém a configuração da implantação e do serviço.

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

Aplique a configuração ao seu cluster:

kubectl apply -f gke_deployment/visit-counter.yaml

Determine o endereço IP externo para este app de amostra executando o seguinte comando:

kubectl get service visit-counter

Verifique se o app está hospedado no IP externo usando o navegador ou envie uma solicitação GET usando curl ou o navegador:

curl http://EXTERNAL_IP

Como remover a entrada de tabelas IP para a instância do Redis

Se você seguiu o passo três da seção destas instruções de Como preparar seu cluster do GKE, você instalou o intervalo de IPs reservados da sua instância do Redis para as tabelas de IP da instância do GKE. Se você quiser remover essa entrada de intervalo de IP do Redis das tabelas de IP da instância do GKE, execute o seguinte comando no diretório k8s-custom-iptables/:

./uninstall.sh