Memorystore
Create a GCP project for attached resources.
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.
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
# }
}
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>"
}
output "valkey_connection" {
value = {
endpoints = google_memorystore_instance.valkey.discovery_endpoints
}
}
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 repositoryemartech/google-cloud-redis-clifor 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.
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.
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 nameredis. 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.
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.
In JVM, this is a bit complicated:
- use
rediss://in url: this tells the client to use TLS (non-TLS isredis://) - use a
ClientOptionswith the appropriate settings:- create a
KeyStorewith the certificate. You have to read the certificate from file.CertificateFactorycan be used to create aCertificatewhich can be passed to aKeyStore. - create an
SslOptionswith- the created
KeyStoreviaTrustManagerFactoryand - an
SSLParameterswith an appropriate hostname set withsetServerNames(This is very important since the certificate is issued globally for the SNI nameredis. This tells the client that whatever your actual Redis hostname is, validate the cert for this name.)
- the created
- use this
SslOptionsto build aClientOptions
- create a
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()
}
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);
}
}
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()
}