App Engine 표준 환경 애플리케이션에서 Redis 인스턴스에 연결

서버리스 VPC 액세스를 사용하여 App Engine 표준 환경에서 Redis 인스턴스에 연결할 수 있습니다. App Engine 앱은 Redis 인스턴스와 동일한 리전에 있어야 합니다.

설정

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

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

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

서버리스 VPC 액세스 구성

App Engine 앱에서 Redis 인스턴스의 승인된 VPC 네트워크에 연결하려면 서버리스 VPC 액세스를 설정해야 합니다.

  1. 다음 명령어를 실행하여 Redis 인스턴스의 승인된 네트워크를 찾습니다.

    gcloud beta redis instances describe [INSTANCE_ID] --region [REGION]
    
  2. 커넥터 만들기의 안내에 따라 서버리스 VPC 액세스 커넥터를 만듭니다. 앱 및 Redis 인스턴스와 동일한 리전에 커넥터를 만들고 커넥터가 Redis 인스턴스의 승인된 VPC 네트워크에 연결되어 있는지 확인합니다. 커넥터의 이름을 기억하세요.

샘플 애플리케이션

이 샘플 HTTP 서버 애플리케이션은 App 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.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)
	}
}

자바

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

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

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>3.6.1</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==2.0.1
gunicorn==20.1.0
redis==3.5.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 '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 and Cloud Run.
    # See entrypoint in app.yaml or Dockerfile.
    app.run(host='127.0.0.1', port=8080, debug=True)

애플리케이션 배포 준비

Redis 인스턴스에 액세스하려면 App Engine 앱을 서버리스 VPC 액세스 커넥터를 사용하도록 구성해야 하며 Redis 인스턴스의 연결 세부정보를 제공해야 합니다.

  1. Redis 인스턴스와 동일한 리전에 App Engine 애플리케이션을 만듭니다.

  2. 서버리스 VPC 액세스 커넥터와 Redis 인스턴스의 IP 주소 및 포트를 지정하도록 앱 구성을 업데이트합니다.

    Go

    gae_standard_deployment/app.yaml 파일을 업데이트합니다.

    runtime: go111
    
    # Update with Redis instance details
    env_variables:
      REDISHOST: '<REDIS_IP>'
      REDISPORT: '6379'
    
    # Update with Serverless VPC Access connector details
    vpc_access_connector:
      name: 'projects/<PROJECT_ID>/locations/<REGION>/connectors/<CONNECTOR_NAME>'

    자세한 내용은 app.yaml 구성 파일을 참조하세요.

    자바

    gae_standard_deployment/appengine-web.xml 파일을 업데이트하여 서버리스 VPC 액세스 커넥터를 지정합니다.

    <appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
      <runtime>java8</runtime>
      <threadsafe>true</threadsafe>
      <vpc-access-connector>
        <name>projects/[PROJECT_ID]/locations/[REGION]/connectors/[CONNECTOR_NAME]</name>
      </vpc-access-connector>
    </appengine-web-app>

    그리고 Redis 인스턴스의 IP 주소와 포트로 src/main/resources/application.properties 파일을 업데이트합니다.

    redis.host=REDIS_HOST_IP
    redis.port=6379

    앱 구성에 대한 자세한 내용은 appengine-web.xml 참조를 확인하세요.

    Node.js

    gae_standard_deployment/app.yaml 파일을 업데이트합니다.

    runtime: nodejs10
    
    # Update with Redis instance details
    env_variables:
      REDISHOST: '<REDIS_IP>'
      REDISPORT: '6379'
    
    # Update with Serverless VPC Access connector details
    vpc_access_connector:
      name: 'projects/<PROJECT_ID>/locations/<REGION>/connectors/<CONNECTOR_NAME>'

    자세한 내용은 app.yaml 구성 파일을 참조하세요.

    Python

    gae_standard_deployment/app.yaml 파일을 업데이트합니다.

    runtime: python37
    entrypoint: gunicorn -b :$PORT main:app
    
    # Update with Redis instance details
    env_variables:
      REDISHOST: '<REDIS_IP>'
      REDISPORT: '6379'
    
    # Update with Serverless VPC Access connector details
    vpc_access_connector:
      name: 'projects/<PROJECT_ID>/locations/<REGION>/connectors/<CONNECTOR_NAME>'

    자세한 내용은 app.yaml 구성 파일을 참조하세요.

App Engine 표준 환경에 애플리케이션 배포

애플리케이션을 배포하려면 다음 안내를 따르세요.

  1. 필요한 구성 파일을 소스 디렉터리에 복사합니다.

    Go

    app.yamlgo.mod 파일을 소스 디렉터리에 복사합니다.

    cp gae_standard_deployment/{app.yaml,go.mod} .
    

    자바

    appengine-web.xml 파일을 소스 디렉터리에 복사합니다.

    mkdir -p src/main/webapp/WEB-INF
    cp gae_standard_deployment/appengine-web.xml src/main/webapp/WEB-INF/
    

    Node.js

    app.yaml 파일을 소스 디렉터리에 복사합니다.

    cp gae_standard_deployment/app.yaml .
    

    Python

    app.yaml 파일을 소스 디렉터리에 복사합니다.

    cp gae_standard_deployment/app.yaml .
    
  2. 배포 명령을 실행합니다.

    Go

    gcloud app deploy
    

    자바

    mvn package appengine:stage
    gcloud app deploy target/appengine-staging/app.yaml
    

    Node.js

    gcloud app deploy
    

    Python

    gcloud app deploy
    

배포가 완료되면 앱을 방문할 수 있는 URL을 명령어가 출력합니다. 이 URL을 방문하면 페이지가 로드될 때마다 Redis 인스턴스의 수가 증가하는 것을 볼 수 있습니다.