Google Kubernetes Engine クラスタから Redis インスタンスに接続する

Redis インスタンスには、Redis インスタンスと同じ承認済みネットワークを使用する Google Kubernetes Engine クラスタからのみ接続できます。

設定

Google Cloud CLI をインストール済みで Redis インスタンスを作成済みの場合は、次の手順をスキップできます。

  1. gcloud CLI をインストールして初期化します。

    gcloud init
    
  2. クイックスタート ガイドの手順に沿って Redis インスタンスを作成します。Redis インスタンスのゾーン、IP アドレス、ポート番号をメモしておきます。

GKE クラスタの準備

  1. GKE クラスタを作成していない場合は、Google Cloud CLI で次のコマンドを使用して作成します。

    1. このサンプル アプリケーションのプロジェクトを gcloud で指定します。

      gcloud config set project PROJECT_ID

    2. gcloud で Compute Engine のゾーン構成変数を設定します。

      gcloud config set compute/zone ZONE

    3. visitcount-cluster という GKE クラスタを作成します。

      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 エイリアスの詳細については IP エイリアスのドキュメントを参照してください。

サンプル アプリケーション

このサンプル HTTP サーバー アプリケーションは、Google Kubernetes Engine クラスタから Redis インスタンスへの接続を確立します。

選択したプログラミング言語のリポジトリのクローンを作成し、サンプルコードを含むフォルダに移動します。

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

このサンプル アプリケーションは、/ エンドポイントがアクセスされるたびに Redis カウンタをインクリメントします。

Go

このアプリケーションは、github.com/gomodule/redigo/redis クライアントを使用します。次のコマンドを実行してインストールします。

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

アプリケーションの例:


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

このアプリケーションは、Jetty 3.1 サーブレット ベースです。

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 モジュールを使用します。

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

このアプリケーションでは、ウェブサービスに Flask を使用し、redis-py パッケージを使用して 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)

コンテナ イメージのビルド

次のように入力し、コンテナ イメージを構築して Container Registry に push します。

    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

アプリケーションを Google Kubernetes Engine にデプロイする

Redis インスタンスの IP がハードコーディングされないようにするため、次のように 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 でホストされているアプリを確認するか、curl またはブラウザを使用して GET リクエストを送信します。

curl http://EXTERNAL_IP

Redis インスタンスの IP テーブル エントリを削除する

このチュートリアルの GKE クラスタの準備セクションの手順 3 を実施した場合は、Redis インスタンスの予約済み IP 範囲が GKE インスタンスの IP テーブルにインストールされています。この Redis IP 範囲エントリを GKE インスタンスの IP テーブルから削除する場合は、k8s-custom-iptables/ ディレクトリから次のコマンドを実行します。

./uninstall.sh