GAP Documentation
GitHub Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Memorystore

Prerequisites

Create a GCP project for attached resources.

Create Valkey Instance

The IaC team is maintaining a valkey module that could be used as a starting point.

module "valkey" {
  source  = "app.terraform.io/sap-emarsys/valkey/google"
  version = "1.0.1"

  project                  = "my-project-id"
  instance_id              = "my-valkey-instance"
  location                 = "my-region"
  shard_count              = 1
  replica_count            = 1
  node_type                = "SHARED_CORE_NANO"
  # https://wiki.one.int.sap/wiki/spaces/itsec/pages/1916766708/Google+Cloud+Platform+-+Security+Hardening#GoogleCloudPlatformSecurityHardening-8.1SecureConfigurationofManagedCachingServices
  # 8.1-2
  transit_encryption_mode  = "SERVER_AUTHENTICATION"
  zone_distribution_config = {
    mode = "MULTI_ZONE"
  }
  desired_auto_created_endpoints = {
    network    = "projects/${var.network_project}/global/networks/${var.network_name}"
    project_id = var.gcp_project_id
  }
}

For configuration options see module documentation, e.g: persistence. Similarly the memorystore resource can be used directly.

Memorystore Valkey is a cache, not persistent storage. Do not rely on it for durable data.

Redis

While valkey is recommended, redis is already used in older projects. As redis won’t work simply with Private Service Connect, it is necessary to configure the project as a service project in resman.

shared_vpc_service_config:
  host_project: $project_ids:network
resource "google_redis_instance" "cache" {
  name           = "ha-memory-cache"
  tier           = "STANDARD_HA"
  memory_size_gb = 1
  
  region  = "europe-west3"
  
  authorized_network      = "projects/${var.network_project}/global/networks/${var.network_name}"
  connect_mode            = "PRIVATE_SERVICE_ACCESS"
  transit_encryption_mode = "SERVER_AUTHENTICATION"
  auth_enabled            = true
  
  redis_version     = "REDIS_7_2"
  display_name      = "Terraform Test Instance"
# (...)
  # lifecycle {
  #   prevent_destroy = true
  # }
}

IAM Authentication

Grant your workload access to Valkey with k8s principal.

The relevant <PROJECT_NUMBER> and <PROJECT_ID> fields to be filled below for the specific clusters where your workload runs can be found here. More on IAM configuration.

# resman style context interpolation
iam:
  roles/memorystore.dbConnectionUser:
  # - $iam_principals:wi/gap/<NAMESPACE>/<KSA_NAME>
  # for example:
    - $iam_principals:wi/gap/cloud-platform/gap-docs

# full principal
iam:
  roles/memorystore.dbConnectionUser:
    - principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<PROJECT_ID>.svc.id.goog/subject/ns/<NAMESPACE>/sa/<KSA_NAME>

or

resource "google_project_iam_member" "valkey_access" {
  project = "my-project-id"
  role    = "roles/memorystore.dbConnectionUser"
  member  = "principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<PROJECT_ID>.svc.id.goog/subject/ns/<NAMESPACE>/sa/<KSA_NAME>"
}

Retrieve Connection Details

output "valkey_connection" {
  value = {
    endpoints = google_memorystore_instance.valkey.discovery_endpoints
  }
}

Interacting with Redis

To simplify connecting to the Cloud Redis instance from a local machine, you can use a CLI tool that automates the process, instead of running the below steps manually. Check the GitHub repository emartech/google-cloud-redis-cli for installation and usage instructions.

If you want to quickly open a CLI into your Redis cluster, first you will need to add the CA certificate to your secrets. You can download the certificate from the Memorystore Redis UI under the Security tab. Either use GAP Secret Editor to add the cert to your app secret (ex. as REDIS_CA_CERT), or use gap-cli, but in that case you have to inline the cert keeping the line breaks using the \n control.

For valkey:

gcloud memorystore instances get-certificate-authority INSTANCE --project PROJECT_ID --location REGION

After that you can run the following command with your namespace and redis IP:

kubectl run -it redis-cli --rm \
-n <your-namespace> \
--labels app.kubernetes.io/name=<your-application-name> \
--overrides='{
  "spec": {
    "securityContext": {
      "runAsUser": 1000,
      "fsGroup": 1000
    },
    "containers": [
      {
        "name": "redis-cli",
        "envFrom": [{ "secretRef": { "name": "<your-application-name>" } }],
        "image": "redis",
        "stdin":true,
        "tty":true,
        "stdinOnce":true,
        "command":["bash"]
      }
    ]
  }
}' --image=redis bash

When the prompt appears create the cert file from the env variable:

echo "$REDIS_CA_CERT" > cacert.pem

and connect to your redis instance with the CLI:

redis-cli -h <redis ip> --cacert cacert.pem --tls -p <redis-port|6378>

You will also need to authenticate if AUTH is enabled (Memorystore):

AUTH <auth-string-of-instance>

The run command will automatically clean up the pod once you exit the CLI.


Configure your application to connect to Redis

This step depends on your language and Redis driver, please refer to the docs of your library. For reference we provide some examples in widely used languages and drivers.

NodeJS and the redis npm package

The following snippet shows how to configure the client to use TLS

const redis = require('redis');
const fs = require('fs')

const main = async () => {
  const client = redis.createClient({
      password: '*****', // (optional) use your AUTH string here if you have AUTH enabled (Memorystore)
      socket: {
          host: '*******', // use the Redis instance hostname or internal IP
          port: 6378,
          tls: true,
          ca: [process.env.REDIS_CA_CERT]
      }
  });

  client.on('error', (err) => console.log('Redis Client Error', err));

  await client.connect();
  console.log("Redis connected");
  
  console.log(await redis.ping()); // -> PONG
}

void main();
  • rejectUnauthorized: tells the client to verify the certificate against the provided root CA.
  • servername: this is very important since the certificate is issued globally for the SNI name redis. This tells the client that whatever your actual Redis hostname is, validate the cert for this name.
  • ca: the actual root CA certificate. This is mounted from a secret to the above location (/ca/ca.crt) in the patch file.

PHP and Predis

Predis supports TLS configuration of the Redis client. Example configuration to connect:

$redis = new Predis\Client(
  [
    "scheme" => "tls",
    "host" => "******", // use the Redis instance hostname or internal IP
    "port" => 6378,
    "password" => "******" // (optional) use your AUTH string here if you have AUTH enabled (Memorystore)
    "ssl" => [
      "cafile" => "private.pem", 
      "verify_peer" => true,
      "peer_name" => "*****" // use `redis` for in-cluster or instance name for Memorystore
    ],
  ]
);
echo "Connected to Redis";
?>

OR you can achieve the same with a connection string (which can be your “REDIS_URL” environment variable, no need to write a parser for it)

$redis = new Predis\Client("tls://hostname:6378?password=auth-string&ssl[cafile]=path/to/private.pem&ssl[peer_name]=instance-name-of-memorystore

As verify_peer is true by default there’s no need to declare it.

Scala, redis4cats, lettuce

In JVM, this is a bit complicated:

  • use rediss:// in url: this tells the client to use TLS (non-TLS is redis://)
  • use a ClientOptions with the appropriate settings:
    • create a KeyStore with the certificate. You have to read the certificate from file. CertificateFactory can be used to create a Certificate which can be passed to a KeyStore.
    • create an SslOptions with
      • the created KeyStore via TrustManagerFactory and
      • an SSLParameters with an appropriate hostname set with setServerNames (This is very important since the certificate is issued globally for the SNI name redis. This tells the client that whatever your actual Redis hostname is, validate the cert for this name.)
    • use this SslOptions to build a ClientOptions
Implementation

for {
  clientOptions <- createClientOptions(redisConfig)
  redis         <- Redis[F].withOptions(url, clientOptions, RedisCodec.Utf8)
} yield redis

private def createClientOptions[F[_]: Sync](redisConfig: RedisConfig): Resource[F, ClientOptions] =
  getCertificate(redisConfig).map { certificate =>
    val keystore            = createKeystoreWithCertificate(certificate)
    val trustManagerFactory = createTrustManagerFactory(keystore)
    val sslOptions          = createSslOptions(trustManagerFactory, redisConfig.sniHostName)

    ClientOptions
      .builder()
      .sslOptions(sslOptions)
      .build()
  }

private def getCertificate[F[_]: Sync](redisConfig: RedisConfig): Resource[F, Certificate] =
  Resource
    .liftF(
      redisConfig.caCert
        .map(Applicative[F].pure(_))
        .getOrElse(Sync[F].delay(Files.readString(Paths.get(redisConfig.caFile))))
    )
    .map(createCertificateFromString)

private def createCertificateFromString(certificate: String): Certificate =
  CertificateFactory
    .getInstance("X.509")
    .generateCertificate(new ByteArrayInputStream(certificate.getBytes))

private def createKeystoreWithCertificate(certificate: Certificate, password: Array[Char] = Array.empty) = {
  val keyStore = KeyStore.getInstance("jks")
  keyStore.load(null, password)
  keyStore.setCertificateEntry("localhost", certificate)
  keyStore
}

private def createTrustManagerFactory(trustStore: KeyStore) = {
  val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
  trustManagerFactory.init(trustStore)
  trustManagerFactory
}

private def createSslOptions[F[_]: Sync](trustManagerFactory: TrustManagerFactory, sniHostNameO: Option[String]) = {
  val sslParameters = new SSLParameters()
  sniHostNameO.foreach { sniHostName =>
    sslParameters.setServerNames(List[SNIServerName](new SNIHostName(sniHostName)).asJava)
  }

  SslOptions
    .builder()
    .trustManager(trustManagerFactory)
    .sslParameters(() => sslParameters)
    .build()
}

Java with Jedis

You have to the same thing as described in the Scala example, but with Jedis specific classes.

Implementation

public Jedis createJedisClient() {
    final URI uri = URI.create("rediss://<your-app-name>-redis:6379");
    final SSLSocketFactory sslSocketFactory = createTrustStoreSslSocketFactory();
    final NoopHostnameVerifier noopHostnameVerifier = new NoopHostnameVerifier();
    final SSLParameters sslParameters = createSslParameters();
    final JedisShardInfo shardInfo = new JedisShardInfo(uri, sslSocketFactory, sslParameters, noopHostnameVerifier);
    return new Jedis(shardInfo);
}

private SSLParameters createSslParameters() {
    SSLParameters sslParameters = new SSLParameters();
    sslParameters.setServerNames(List.of(new SNIHostName("redis")));
    return sslParameters;
}

private SSLSocketFactory createTrustStoreSslSocketFactory() {
    try {
        InputStream inputStream = new FileInputStream("/ca/ca.crt");
        Certificate certificate = CertificateFactory.getInstance("X.509")
            .generateCertificate(new ByteArrayInputStream(inputStream.readAllBytes()));
        KeyStore keyStore = KeyStore.getInstance("jks");
        keyStore.load(null, new char[0]);
        keyStore.setCertificateEntry("redis", certificate);

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagers, new SecureRandom());
        return sslContext.getSocketFactory();
    } catch (Exception exception) {
        throw new RuntimeException(exception);
    }
}

Connecting to redis in Go using go-redis

To install go-redis, issue the command:

go get github.com/go-redis/redis/v8

In order to connect to a redis via TLS, the following three files are required (these are provided as k8s secrets on GAP):

  • ca.crt
  • redis.crt
  • redis.key
Implementation

package main

import (
  "crypto/tls"
  "crypto/x509"
  "github.com/go-redis/redis/v8"
)

func getRedisConfig(certificateFilePath, keyFilePath, caFilePath) *redis.Options {
	cert, err := tls.LoadX509KeyPair(certificateFilePath, keyFilePath)
	if err != nil {
		log.Fatal(err)
	}

	caCert, err := ioutil.ReadFile(c.CAFilePath)
	if err != nil {
		log.Fatal(err)
	}
	rootCAPool := x509.NewCertPool()
	rootCAPool.AppendCertsFromPEM(caCert)

	redisConfig := &redis.Options{
		Addr: fmt.Sprintf("%s:%d", c.RedisHostname, c.RedisPort),
		DB:   0,
		TLSConfig: &tls.Config{
			ServerName:   c.RedisHostname,
			Certificates: []tls.Certificate{cert},
			RootCAs:      rootCAPool,
		},
	}
	if c.RedisPassword != "" {
		redisConfig.Password = c.RedisPassword
	}
	return redisConfig
}

func main() {
  config := getRedisConfig("/certs/redis.crt", "/certs/redis.key", "/ca/ca.crt");
  redisClient := redis.NewClient(config)
  defer redisClient.Close()
}