Google Kubernetes Engine 클러스터에서 Redis 인스턴스에 연결

인스턴스와 동일한 지역에 있고 동일한 네트워크를 사용하는 Google Kubernetes Engine 클러스터에서 Redis 인스턴스에 연결할 수 있습니다.

설정

이미 Cloud SDK를 설치하고 Redis 인스턴스를 만들었다면 다음 단계를 건너뛸 수 있습니다.

  1. Cloud SDK를 설치하고 초기화합니다.

    gcloud init
    
  2. 빠른 시작 가이드에 따라 Redis 인스턴스를 만듭니다. Redis 인스턴스의 영역, IP 주소, 포트를 기록합니다.

GKE 클러스터 준비

  1. Redis 인스턴스와 동일한 리전에 GKE 클러스터를 만들지 않았다면 gcloud 명령줄 도구에 다음 명령어를 사용하여 GKE 클러스터를 만듭니다.

    1. 샘플 애플리케이션의 프로젝트를 gcloud에 지정합니다.

      gcloud config set project [PROJECT_ID]

    2. gcloud에서 Compute Engine 영역 구성 변수를 설정합니다. Redis 인스턴스와 동일한 지역의 영역을 선택합니다. Redis 인스턴스와 동일한 영역이 아니어도 되지만 동일해도 무방합니다.

      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 별칭에 대한 자세한 내용을 확인할 수 있습니다.

샘플 애플리케이션

이 샘플 HTTP 서버 애플리케이션은 Google Kubernetes Engine 클러스터에서 Redis 인스턴스로 연결을 설정합니다.

원하는 프로그래밍 언어의 저장소를 클론하고 샘플 코드가 포함된 폴더로 이동합니다.

Go

git clone https://github.com/GoogleCloudPlatform/golang-samples
cd golang-samples/memorystore/redis

자바

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

// 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.NewPool(func() (redis.Conn, error) {
		return redis.Dial("tcp", redisAddr)
	}, maxConnections)

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

자바

이 애플리케이션은 Jetty 3.1 서블릿 기반입니다.

Jedis 라이브러리를 사용합니다.

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>3.3.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": ">=8.0.0"
  },
  "dependencies": {
    "redis": "^3.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 인스턴스와 통신하기 위해 redis-py 패키지를 사용합니다.

Flask==1.1.2
gunicorn==20.0.4
redis==3.5.0
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 'Visitor number: {}'.format(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. See entrypoint in app.yaml.
    app.run(host='127.0.0.1', port=8080, debug=True)

컨테이너 이미지 빌드

컨테이너 이미지를 빌드하여 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

Google Kubernetes Engine에 애플리케이션 배포

<PROJECT_ID>를 Google Cloud 프로젝트 ID로 바꾸어 gke_deployment/visit-counter.yaml을 업데이트합니다. 이 파일에는 배포 및 서비스의 구성이 포함되어 있습니다.

Go

Redis 인스턴스 IP를 하드 코딩하지 않으려면 redishost ConfigMap을 만듭니다.

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

다음 명령어를 사용하여 구성을 확인합니다.

    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

자바

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

Redis 인스턴스 IP를 하드 코딩하지 않으려면 redishost ConfigMap을 만듭니다.

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

다음 명령어를 사용하여 구성을 확인합니다.

    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

Redis 인스턴스 IP를 하드 코딩하지 않으려면 redishost ConfigMap을 만듭니다.

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

다음 명령어를 사용하여 구성을 확인합니다.

    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

클러스터에 구성을 적용합니다.

    kubectl apply -f gke_deployment/visit-counter.yaml

다음 명령어를 실행하여 이 샘플 앱의 [EXTERNAL-IP] 주소를 확인합니다.

    kubectl get service visit-counter

브라우저를 통해 http://[EXTERNAL-IP]에서 호스팅되는 앱을 보거나 cURL 또는 브라우저를 통해 GET 요청을 보냅니다.

    curl http://[EXTERNAL-IP]

Redis 인스턴스의 IP 테이블 항목 삭제

이 둘러보기의 GKE 클러스터 준비 섹션에서 세 번째 단계를 거치면 Redis 인스턴스의 예약된 IP 범위가 GKE 인스턴스의 IP 테이블에 설치됩니다. GKE 인스턴스의 IP 테이블에서 이 Redis IP 범위 항목을 삭제하려면 k8s-custom-iptables/ 디렉터리에서 다음 명령어를 실행합니다.

    ./uninstall.sh