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

Só pode estabelecer ligação à sua instância do Redis a partir de clusters do Google Kubernetes Engine que usem a mesma rede autorizada que a instância do Redis.

Configuração

Se já instalou a CLI Google Cloud e criou uma instância do Redis, pode ignorar estes passos.

  1. Instale a CLI gcloud e inicialize-a:

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

A preparar o seu cluster do GKE

  1. Se não tiver criado um cluster do GKE, crie um através dos seguintes comandos para a Google Cloud CLI:

    1. Designar o projeto para esta aplicação de exemplo em gcloud.

      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. Cria um cluster do GKE denominado visitcount-cluster.

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

  2. Se não criou o cluster com gcloud, use o seguinte comando para obter as credenciais do cluster:

    gcloud container clusters get-credentials CLUSTER_NAME --zone CLUSTER_ZONE --project PROJECT_ID
    1. CLUSTER_NAME é o nome do seu cluster do GKE.
    2. CLUSTER_ZONE é a zona em que o cluster se encontra.
    3. PROJECT_ID é o projeto onde existem o cluster e as instâncias do Redis.
  3. Se o cluster tiver a versão 1.8 ou superior e tiver aliases de IP ativados, ignore este passo. Se o seu cluster for a versão 1.7 ou inferior, ou o cluster da versão 1.8 ou superior não tiver aliases de IP ativados, siga estes passos de solução alternativa antes de tentar estabelecer ligação à sua instância.

    1. Execute estes comandos, substituindo RESERVED_IP_RANGE pelo intervalo de IP reservado da sua 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 não souber o intervalo de IPs reservados da sua instância, descubra-o através da consola (opções avançadas) ou do seguinte comando:

      gcloud redis instances describe INSTANCE_ID --region=REGION
      

    Para mais informações sobre os alias de IP, incluindo como criar um cluster com esta definição ativada, consulte a documentação sobre os alias de IP.

Aplicação de exemplo

Esta aplicação de servidor HTTP de exemplo estabelece uma ligação a uma instância do Redis a partir de um cluster do Google Kubernetes Engine.

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

Ir

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 aplicação de exemplo incrementa um contador do Redis sempre que o ponto final / é acedido.

Ir

Esta aplicação usa o cliente github.com/gomodule/redigo/redis. Instale-o executando o seguinte comando:

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

Exemplo de aplicação:


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

Usa a biblioteca Jedis:

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

A classe AppServletContextListener é usada para criar um conjunto de ligações 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 Web que incrementa um contador 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 aplicação usa o 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 aplicação usa o Flask para o serviço Web e o pacote redis-py para 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)

Criar a imagem do contentor

Crie e envie a imagem de contentor 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

Implementar a sua aplicação no Google Kubernetes Engine

Para evitar a programação rígida do IP da instância do Redis, pode criar um redishost ConfigMap:

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

Valide a configuração através do 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 de contentor criada em Criar a imagem de contentor. Este ficheiro contém a configuração para a implementação e o 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 cluster:

kubectl apply -f gke_deployment/visit-counter.yaml

Determine o endereço IP externo desta app de exemplo executando o seguinte comando:

kubectl get service visit-counter

Valide se a sua app está alojada no IP externo através do navegador ou envie um pedido GET através do curl ou do navegador:

curl http://EXTERNAL_IP

Remover a entrada das tabelas de IP para a instância do Redis

Se seguiu o passo três da secção deste guia passo a passo de Preparar o cluster do GKE, instalou o intervalo de IPs reservado da sua instância do Redis nas tabelas de IP da instância do GKE. Se quiser remover esta entrada do intervalo de IPs do Redis das tabelas de IPs da sua instância do GKE, execute o seguinte comando a partir do diretório k8s-custom-iptables/:

./uninstall.sh