App Engine スタンダード環境のアプリケーションから Redis インスタンスへの接続

サーバーレス VPC アクセスを使用すると、App Engine スタンダード環境から Redis インスタンスに接続できます。

設定

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

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

    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 インスタンスで承認済みの VPC ネットワークにコネクタが接続されていることを確認してください。コネクタの名前を覚えておいてください。

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

このサンプル HTTP サーバー アプリケーションは、App 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

// 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.0
gunicorn==22.0.0
redis==5.0.1
Werkzeug==3.0.1
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)

デプロイメントに向けたアプリケーションの準備

Redis インスタンスにアクセスするには、サーバーレス VPC アクセス コネクタを使用するように App Engine アプリを構成し、Redis インスタンスの接続の詳細を指定する必要があります。

  1. 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 構成ファイルをご覧ください。

    Java

    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>

    src/main/resources/application.properties ファイルを Redis インスタンスの IP アドレスとポートで更新します。

    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.yaml ファイルと go.mod ファイルをソース ディレクトリにコピーします。

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

    Java

    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. deploy コマンドを実行します。

    Go

    gcloud app deploy
    

    Java

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

    Node.js

    gcloud app deploy
    

    Python

    gcloud app deploy
    

デプロイが完了すると、コマンドにより、アプリにアクセスするための URL が出力されます。この URL にアクセスすると、ページが読み込まれるたびに Redis インスタンスのカウントが増加するのを確認できます。