Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,52 @@ spec:
| maxMemory | Redis Node의 Max Memory 설정 |
| resources | Redis Node Container의 리소스 설정 |
| exporterResources | Redis Node Exporter Container의 리소스 설정 |
| redisConfig | Custom redis.conf 설정 (map[string]string) |

### Custom Redis Configuration

사용자가 요구하는 redis.conf 설정을 적용하여 Redis Cluster를 구성할 수 있습니다.
`redisConfig` 필드에 key-value 형태로 설정을 지정하면, 해당 설정이 redis.conf 파일로 생성되어 모든 Redis Node에 적용됩니다.

**기본 설정**

다음 설정들은 클러스터 운영에 필수적이므로 자동으로 추가됩니다 (사용자가 명시하지 않은 경우):
- `port`: Redis Node의 포트 번호
- `cluster-enabled`: 클러스터 모드 활성화 (yes)
- `cluster-port`: 클러스터 버스 포트
- `cluster-node-timeout`: 클러스터 노드 타임아웃 (5000ms)
- `maxmemory`: 최대 메모리 설정
- `protected-mode`: 보호 모드 (no)

**사용 예시**

```yaml
apiVersion: redis.redis/v1beta1
kind: RedisCluster
metadata:
name: rediscluster-custom
spec:
image: redis
tag: "7.0"
masters: 3
replicas: 1
basePort: 10000
maxMemory: "512mb"
# Custom redis.conf 설정
redisConfig:
maxmemory-policy: "allkeys-lru"
timeout: "300"
tcp-keepalive: "60"
save: "900 1 300 10"
appendonly: "yes"
appendfsync: "everysec"
exporter:
enabled: true
```

**주의사항**
- 클러스터 운영에 필수적인 설정(port, cluster-enabled 등)을 사용자가 지정하는 경우, 사용자가 지정한 값이 우선 적용됩니다.
- 잘못된 설정은 Redis Node의 시작 실패를 초래할 수 있으므로 주의가 필요합니다.

### How to Access Redis Node

Expand Down
1 change: 1 addition & 0 deletions api/v1beta1/rediscluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type RedisClusterSpec struct {
Affinity *corev1.Affinity `json:"affinity,omitempty"`
Persistence *PersistenceSpec `json:"persistence,omitempty"`
Exporter *ExporterSpec `json:"exporter,omitempty"`
RedisConfig map[string]string `json:"redisConfig,omitempty"`
}

// RedisClusterStatus defines the observed state of RedisCluster
Expand Down
7 changes: 7 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions charts/redis-operator/crds/redis.redis_redisclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,10 @@ spec:
required:
- enabled
type: object
redisConfig:
additionalProperties:
type: string
type: object
replicas:
format: int32
type: integer
Expand Down
4 changes: 4 additions & 0 deletions charts/redis-operator/templates/rediscluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ spec:
affinity:
{{- toYaml .Values.redisCluster.affinity | nindent 4 }}
{{- end }}
{{- if .Values.redisCluster.redisConfig }}
redisConfig:
{{- toYaml .Values.redisCluster.redisConfig | nindent 4 }}
{{- end }}
exporter:
enabled: {{ .Values.redisCluster.exporter.enabled }}
image: {{ .Values.redisCluster.exporter.image }}:{{ .Values.redisCluster.exporter.tag }}
Expand Down
9 changes: 9 additions & 0 deletions charts/redis-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ redisCluster:
# cpu: 100m
# memory: 128Mi

# Custom redis.conf configuration
# Each key-value pair will be added to redis.conf
# Example:
# redisConfig:
# maxmemory-policy: "allkeys-lru"
# timeout: "300"
# tcp-keepalive: "60"
redisConfig: {}

# persistence:
# enabled: true
# storageClass: ""
Expand Down
4 changes: 4 additions & 0 deletions config/crd/bases/redis.redis_redisclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,10 @@ spec:
required:
- enabled
type: object
redisConfig:
additionalProperties:
type: string
type: object
replicas:
format: int32
type: integer
Expand Down
15 changes: 14 additions & 1 deletion config/samples/redis_v1beta1_rediscluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,17 @@ metadata:
app.kubernetes.io/created-by: redis-operator
name: rediscluster-sample
spec:
# TODO(user): Add fields here
image: redis
tag: "7.0"
masters: 3
replicas: 1
basePort: 10000
maxMemory: "512mb"
# Custom redis.conf configuration (optional)
# redisConfig:
# maxmemory-policy: "allkeys-lru"
# timeout: "300"
# tcp-keepalive: "60"
# save: "900 1 300 10"
exporter:
enabled: true
29 changes: 29 additions & 0 deletions config/samples/redis_v1beta1_rediscluster_with_custom_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
apiVersion: redis.redis/v1beta1
kind: RedisCluster
metadata:
labels:
app.kubernetes.io/name: rediscluster
app.kubernetes.io/instance: rediscluster-custom-config
app.kubernetes.io/part-of: redis-operator
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/created-by: redis-operator
name: rediscluster-custom-config
spec:
image: redis
tag: "7.0"
masters: 3
replicas: 1
basePort: 10000
maxMemory: "512mb"
# Custom redis.conf configuration
# These key-value pairs will be written to redis.conf
# Essential cluster settings (port, cluster-enabled, etc.) are automatically added if not specified
redisConfig:
maxmemory-policy: "allkeys-lru"
timeout: "300"
tcp-keepalive: "60"
save: "900 1 300 10"
appendonly: "yes"
appendfsync: "everysec"
exporter:
enabled: true
169 changes: 160 additions & 9 deletions k8sutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,51 @@ func ExtractPortFromPodName(podName string) int32 {
return int32(port)
}

// CreateOrUpdateRedisConfigMap creates or updates the ConfigMap for custom redis configuration
func CreateOrUpdateRedisConfigMap(ctx context.Context, k8scl kubernetes.Interface, redisCluster *redisv1beta1.RedisCluster, logger logr.Logger, port int32) error {
if len(redisCluster.Spec.RedisConfig) == 0 {
return nil // No custom config, skip ConfigMap creation
}

configMap := GenerateRedisConfigMap(redisCluster, port)
configMapName := configMap.Name

// Try to get existing ConfigMap
existingConfigMap, err := k8scl.CoreV1().ConfigMaps(redisCluster.Namespace).Get(ctx, configMapName, metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
// Create new ConfigMap
_, err = k8scl.CoreV1().ConfigMaps(redisCluster.Namespace).Create(ctx, configMap, metav1.CreateOptions{})
if err != nil {
logger.Error(err, "Failed to create ConfigMap", "ConfigMap", configMapName)
return err
}
logger.Info("ConfigMap created successfully", "ConfigMap", configMapName)
return nil
}
logger.Error(err, "Failed to get ConfigMap", "ConfigMap", configMapName)
return err
}

// Update existing ConfigMap
existingConfigMap.Data = configMap.Data
_, err = k8scl.CoreV1().ConfigMaps(redisCluster.Namespace).Update(ctx, existingConfigMap, metav1.UpdateOptions{})
if err != nil {
logger.Error(err, "Failed to update ConfigMap", "ConfigMap", configMapName)
return err
}
logger.Info("ConfigMap updated successfully", "ConfigMap", configMapName)
return nil
}

// CreateMasterPod creates a Redis master Pod with the given port
func CreateMasterPod(ctx context.Context, k8scl kubernetes.Interface, redisCluster *redisv1beta1.RedisCluster, logger logr.Logger, port int32) error {
// Create or update ConfigMap if custom redis config is provided
if err := CreateOrUpdateRedisConfigMap(ctx, k8scl, redisCluster, logger, port); err != nil {
logger.Error(err, "Failed to create or update ConfigMap")
return err
}

podName := fmt.Sprintf("rediscluster-%s-%d", redisCluster.Name, port)
masterPod := GenerateRedisPodDef(redisCluster, port, "")
_, err := k8scl.CoreV1().Pods(redisCluster.Namespace).Create(ctx, masterPod, metav1.CreateOptions{})
Expand Down Expand Up @@ -100,6 +143,12 @@ func CreateMasterPod(ctx context.Context, k8scl kubernetes.Interface, redisClust

// CreateReplicaPod creates a Redis replica Pod attached to the specified master node
func CreateReplicaPod(ctx context.Context, k8scl kubernetes.Interface, redisCluster *redisv1beta1.RedisCluster, logger logr.Logger, port int32, masterNodeID string) error {
// Create or update ConfigMap if custom redis config is provided
if err := CreateOrUpdateRedisConfigMap(ctx, k8scl, redisCluster, logger, port); err != nil {
logger.Error(err, "Failed to create or update ConfigMap")
return err
}

podName := fmt.Sprintf("rediscluster-%s-%d", redisCluster.Name, port)
replicaPod := GenerateRedisPodDef(redisCluster, port, masterNodeID)
_, err := k8scl.CoreV1().Pods(redisCluster.Namespace).Create(ctx, replicaPod, metav1.CreateOptions{})
Expand Down Expand Up @@ -187,6 +236,79 @@ func UpdatePodLabelWithRedisID(ctx context.Context, k8scl kubernetes.Interface,
return nil
}

// GenerateRedisConfigMap creates a ConfigMap for custom redis configuration
func GenerateRedisConfigMap(redisCluster *redisv1beta1.RedisCluster, port int32) *corev1.ConfigMap {
configMapName := fmt.Sprintf("rediscluster-%s-config", redisCluster.Name)

// Build redis.conf content from redisConfig map
var redisConf strings.Builder

// Add essential cluster configuration if not already present
hasPort := false
hasClusterEnabled := false
hasClusterPort := false
hasClusterNodeTimeout := false
hasMaxMemory := false
hasProtectedMode := false

for key, value := range redisCluster.Spec.RedisConfig {
if key == "port" {
hasPort = true
}
if key == "cluster-enabled" {
hasClusterEnabled = true
}
if key == "cluster-port" {
hasClusterPort = true
}
if key == "cluster-node-timeout" {
hasClusterNodeTimeout = true
}
if key == "maxmemory" {
hasMaxMemory = true
}
if key == "protected-mode" {
hasProtectedMode = true
}
redisConf.WriteString(fmt.Sprintf("%s %s\n", key, value))
}

// Add required cluster settings if not provided by user
if !hasPort {
redisConf.WriteString(fmt.Sprintf("port %d\n", port))
}
if !hasClusterEnabled {
redisConf.WriteString("cluster-enabled yes\n")
}
if !hasClusterPort {
redisConf.WriteString(fmt.Sprintf("cluster-port %d\n", port+10000))
}
if !hasClusterNodeTimeout {
redisConf.WriteString("cluster-node-timeout 5000\n")
}
if !hasMaxMemory {
redisConf.WriteString(fmt.Sprintf("maxmemory %s\n", redisCluster.Spec.Maxmemory))
}
if !hasProtectedMode {
redisConf.WriteString("protected-mode no\n")
}

configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: redisCluster.Namespace,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(redisCluster, redisv1beta1.GroupVersion.WithKind("RedisCluster")),
},
},
Data: map[string]string{
"redis.conf": redisConf.String(),
},
}

return configMap
}

// GenerateRedisPodDef generates a Pod definition for a Redis instance
func GenerateRedisPodDef(redisCluster *redisv1beta1.RedisCluster, port int32, matchMasterNodeID string) *corev1.Pod {
podName := fmt.Sprintf("rediscluster-%s-%d", redisCluster.Name, port)
Expand All @@ -196,6 +318,24 @@ func GenerateRedisPodDef(redisCluster *redisv1beta1.RedisCluster, port int32, ma
image = fmt.Sprintf("%s:%s", redisCluster.Spec.Image, redisCluster.Spec.Tag)
}

// Build redis-server command
redisCommand := []string{"redis-server"}

// If custom redis config is provided, use ConfigMap mount
if len(redisCluster.Spec.RedisConfig) > 0 {
redisCommand = append(redisCommand, "/redis-config/redis.conf")
} else {
// Use default command-line arguments
redisCommand = append(redisCommand,
"--port", fmt.Sprintf("%d", port),
"--cluster-enabled", "yes",
"--cluster-port", fmt.Sprintf("%d", port+10000),
"--cluster-node-timeout", "5000",
"--maxmemory", redisCluster.Spec.Maxmemory,
"--protected-mode", "no",
)
}

pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Expand Down Expand Up @@ -223,15 +363,7 @@ func GenerateRedisPodDef(redisCluster *redisv1beta1.RedisCluster, port int32, ma
Name: "redis-bus",
},
},
Command: []string{
"redis-server",
"--port", fmt.Sprintf("%d", port),
"--cluster-enabled", "yes",
"--cluster-port", fmt.Sprintf("%d", port+10000),
"--cluster-node-timeout", "5000",
"--maxmemory", redisCluster.Spec.Maxmemory,
"--protected-mode", "no",
},
Command: redisCommand,
ReadinessProbe: GenerateRedisProbe(port),
LivenessProbe: GenerateRedisProbe(port),
},
Expand Down Expand Up @@ -277,6 +409,25 @@ func GenerateRedisPodDef(redisCluster *redisv1beta1.RedisCluster, port int32, ma
pod.Spec.Containers[0].Resources = *redisCluster.Spec.Resources
}

// Add ConfigMap volume and mount if custom redis config is provided
if len(redisCluster.Spec.RedisConfig) > 0 {
configMapName := fmt.Sprintf("rediscluster-%s-config", redisCluster.Name)
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
Name: "redis-config",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: configMapName,
},
},
},
})
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
Name: "redis-config",
MountPath: "/redis-config",
})
}

// Add imagePullSecrets if specified
if len(redisCluster.Spec.ImagePullSecrets) > 0 {
pod.Spec.ImagePullSecrets = redisCluster.Spec.ImagePullSecrets
Expand Down