Skip to content

Commit

Permalink
Add Logstash keystore (#7024)
Browse files Browse the repository at this point in the history
This commit adds support for keystore to Logstash operator.

The key values in keystore are available to Logstash pipelines as environment variables, which can resolve by ${KEY} notation. The keystore can be password protected by setting an environment variable called LOGSTASH_KEYSTORE_PASS. The password is expected to be declared in the main container in env.

A known issue is that the keystore command logstash-keystore is very slow in proportion to the number of key values to add. In my local machine, adding 10 keys needs 6 minutes to start Logstsah.

Adding or updating key values in keystore triggers pod rotation, while deleting a key does not.

This commit adds e2e tests TestLogstashKeystoreWithoutPassword and TestLogstashKeystoreWithPassword



Co-authored-by: Rob Bavey <rob.bavey@elastic.co>
Co-authored-by: Peter Brachwitz <peter.brachwitz@elastic.co>
  • Loading branch information
3 people committed Aug 4, 2023
1 parent d22d0a0 commit cce77a3
Show file tree
Hide file tree
Showing 6 changed files with 351 additions and 19 deletions.
89 changes: 72 additions & 17 deletions docs/orchestrating-elastic-stack-applications/logstash.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,8 @@ spec:
Changes, such as storage class or volume size, are currently forbidden in `spec.volumeClaimTemplates`.
To make these changes, you have to fully delete the {ls} resource, delete and recreate or resize the volume, and create a new {ls} resource.

Before deleting or resizing a persistent queue (PQ) volume, ensure that the queue is empty.
When using the PQ, we recommend setting `queue.drain: true` on the {ls} Pods to ensure that the queue is drained when Pods are shutdown.
Before deleting or resizing a persistent queue (PQ) volume, ensure that the queue is empty.
When using the PQ, we recommend setting `queue.drain: true` on the {ls} Pods to ensure that the queue is drained when Pods are shutdown.
Note that you should also increase the `terminationGracePeriodSeconds` to a large enough value to allow the queue to drain.

This example shows how to configure a {ls} resource to drain the queue and increase the termination grace period.
Expand All @@ -463,11 +463,11 @@ spec:
terminationGracePeriodSeconds: 604800
----

NOTE: A https://github.com/kubernetes/kubernetes/issues/94435[{k8s} known issue]: {k8s} may not honor `terminationGracePeriodSeconds` settings greater than 600.
NOTE: A https://github.com/kubernetes/kubernetes/issues/94435[{k8s} known issue]: {k8s} may not honor `terminationGracePeriodSeconds` settings greater than 600.
A queue of a terminated Pod may not be fully drained, even when `queue.drain: true` is set and a high `terminationGracePeriodSeconds` is configured.

NOTE: In this technical preview, there is currently no way to drain a dead letter queue (DLQ) automatically before {ls} shuts down.
To manually drain the queue, first stop sending data to it, by either disabling the DLQ feature, or disabling any pipelines that send to a DLQ.
NOTE: In this technical preview, there is currently no way to drain a dead letter queue (DLQ) automatically before {ls} shuts down.
To manually drain the queue, first stop sending data to it, by either disabling the DLQ feature, or disabling any pipelines that send to a DLQ.
Then wait for events to stop flowing through any pipelines reading from the input.


Expand All @@ -479,10 +479,10 @@ If you are not concerned about data loss, you can use an `emptyDir` volume for L

[CAUTION]
--
The use of `emptyDir` in a production environment may cause permanent data loss.
The use of `emptyDir` in a production environment may cause permanent data loss.
Do not use with persistent queues (PQs), dead letter queues (DLQs), or with any plugin that requires persistent storage to keep track of state between restarts of {ls}.

Plugins that require persistent storage include any plugin that stores state locally.
Plugins that require persistent storage include any plugin that stores state locally.
These plugins typically have a configuration parameter that includes the name `path` or `directory`, not including paths to static content, such as certificates or keystores.
Examples include the `sincedb_path` setting for the `file`, `dead_letter_queue` and `s3` inputs, the `last_run_metadata_path` for the `JDBC` input, `aggregate_maps_path` for the `aggregate` filter, and `temporary_directory` for the `s3` output, used to aggregate content before uploading to s3.
--
Expand Down Expand Up @@ -531,9 +531,7 @@ The Logstash ECK operator creates a user called `eck_logstash_user_role` when an
}

```


You can <<{p}-users-and-roles,update user permissions>> to include more indices if the Elasticsearch plugin is expected to use indices other than the default. See the <<{p}-logstash-configuration-custom-index, Logstash configuration with a custom index>> sample configuration that creates a user that writes to a custom index.
You can <<{p}-users-and-roles,update user permissions>> to include more indices if the Elasticsearch plugin is expected to use indices other than the default. Check out <<{p}-logstash-configuration-custom-index, Logstash configuration with a custom index>> sample configuration that creates a user that writes to a custom index.
--

This example demonstrates how to create a Logstash deployment that connects to
Expand Down Expand Up @@ -763,8 +761,8 @@ kubectl apply -f {logstash_recipes}/logstash-volumes.yaml
Deploys Logstash, Beats and Elasticsearch. Logstash is configured with two pipelines:

* a main pipeline for reading from the {beats} instance, which will send to the DLQ if it is unable to write to Elasticsearch
* a second pipeline, that will read from the DLQ.
In addition, persistent queues are set up.
* a second pipeline, that will read from the DLQ.
In addition, persistent queues are set up.
This example shows how to configure persistent volumes outside of the default `logstash-data` persistent volume.


Expand Down Expand Up @@ -816,6 +814,63 @@ spec:
----
<1> This will change the maximum and minimum heap size of the JVM on each pod to 2GB

[id="{p}-logstash-keystore"]
=== Setting keystore

You can specify sensitive settings with Kubernetes secrets. ECK automatically injects these settings into the keystore before it starts Logstash.
The ECK operator continues to watch the secrets for changes and will restart Logstash Pods when it detects a change.

NOTE: For the technical preview, the use of settings in the Logstash keystore may impact startup time for Logstash Pods. Startup time will increase linearly for each entry added to the keystore, and this could extend startup time significantly.

The Logstash Keystore can be password protected by setting an environment variable called `LOGSTASH_KEYSTORE_PASS`. Check out https://www.elastic.co/guide/en/logstash/current/keystore.html#keystore-password[Logstash Keystore] documentation for details.

[source,yaml,subs="attributes,+macros,callouts"]
----
apiVersion: v1
kind: Secret
metadata:
name: logstash-keystore-pass
stringData:
LOGSTASH_KEYSTORE_PASS: changed <1>
---
apiVersion: v1
kind: Secret
metadata:
name: logstash-secure-settings
stringData:
HELLO: Hallo
---
apiVersion: logstash.k8s.elastic.co/v1alpha1
kind: Logstash
metadata:
name: logstash-sample
spec:
version: 8.8.0
count: 1
pipelines:
- pipeline.id: main
config.string: |-
input { exec { command => 'uptime' interval => 10 } }
filter {
if ("${HELLO:}" != "") { <2>
mutate { add_tag => ["awesome"] }
}
}
secureSettings:
- secretName: logstash-secure-settings
podTemplate:
spec:
containers:
- name: logstash
env:
- name: LOGSTASH_KEYSTORE_PASS
valueFrom:
secretKeyRef:
name: logstash-keystore-pass
key: LOGSTASH_KEYSTORE_PASS
----
<1> Value of password to protect the Logstash keystore
<2> The syntax for referencing keys is identical to the syntax for environment variables

[id="{p}-logstash-scaling-logstash"]
== Scaling Logstash
Expand Down Expand Up @@ -859,13 +914,13 @@ Note that this release is a technical preview. It is still under active developm
NOTE: Persistence (experimental) is a breaking change from version 2.8.0 of the ECK operator and requires re-creation of existing {ls} resources.

The operator now includes support for persistence.
It creates a small (`1Gi`) default `PersistentVolume` called `logstash-data` that maps to `/usr/share/logstash/data`, typically used for storage from plugins.
The default volume can be overridden by adding a `spec.volumeClaimTemplate` section named `logstash-data` to add more storage, or to use a different `storageClass` from the default, for example.
It creates a small (`1Gi`) default `PersistentVolume` called `logstash-data` that maps to `/usr/share/logstash/data`, typically used for storage from plugins.
The default volume can be overridden by adding a `spec.volumeClaimTemplate` section named `logstash-data` to add more storage, or to use a different `storageClass` from the default, for example.
You can define additional `persistentVolumeClaims` in `spec.volumeClaimTemplate` for use with PQ, or DLQ, for example.

The current implementation does not allow resizing of volumes, even if your chosen storage class would support it.
To resize a volume, delete the {ls} resource, delete and recreate (or resize) the volume, and create a new {ls} resource.
Note that volume claims will not be deleted when you delete the {ls} resource, and must be deleted manually.
The current implementation does not allow resizing of volumes, even if your chosen storage class would support it.
To resize a volume, delete the {ls} resource, delete and recreate (or resize) the volume, and create a new {ls} resource.
Note that volume claims will not be deleted when you delete the {ls} resource, and must be deleted manually.
This behavior might change in future versions of the ECK operator.

[id="{p}-logstash-technical-preview-elasticsearchref"]
Expand Down
11 changes: 9 additions & 2 deletions pkg/controller/logstash/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ package logstash

import (
"context"

"hash/fnv"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"

logstashv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/logstash/v1alpha1"
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/keystore"
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/operator"
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/reconciler"
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/tracing"
Expand All @@ -35,7 +35,8 @@ type Params struct {
Logstash logstashv1alpha1.Logstash
Status logstashv1alpha1.LogstashStatus

OperatorParams operator.Parameters
OperatorParams operator.Parameters
KeystoreResources *keystore.Resources
}

// K8sClient returns the Kubernetes client.
Expand Down Expand Up @@ -99,6 +100,12 @@ func internalReconcile(params Params) (*reconciler.Results, logstashv1alpha1.Log
params.Logstash.Spec.VolumeClaimTemplates = volume.AppendDefaultPVCs(params.Logstash.Spec.VolumeClaimTemplates,
params.Logstash.Spec.PodTemplate.Spec)

if keystoreResources, err := reconcileKeystore(params, configHash); err != nil {
return results.WithError(err), params.Status
} else if keystoreResources != nil {
params.KeystoreResources = keystoreResources
}

podTemplate, err := buildPodTemplate(params, configHash)
if err != nil {
return results.WithError(err), params.Status
Expand Down
78 changes: 78 additions & 0 deletions pkg/controller/logstash/keystore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package logstash

import (
"hash"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"

logstashv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/logstash/v1alpha1"
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/keystore"
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/logstash/volume"
)

const (
KeystorePassKey = "LOGSTASH_KEYSTORE_PASS" // #nosec G101
)

var (
keystoreCommand = "echo 'y' | /usr/share/logstash/bin/logstash-keystore"
initContainersParameters = keystore.InitContainerParameters{
KeystoreCreateCommand: keystoreCommand + " create",
KeystoreAddCommand: keystoreCommand + ` add "$key" --stdin < "$filename"`,
SecureSettingsVolumeMountPath: keystore.SecureSettingsVolumeMountPath,
KeystoreVolumePath: volume.ConfigMountPath,
Resources: corev1.ResourceRequirements{
Requests: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceMemory: resource.MustParse("1Gi"),
corev1.ResourceCPU: resource.MustParse("1000m"),
},
Limits: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceMemory: resource.MustParse("1Gi"),
corev1.ResourceCPU: resource.MustParse("1000m"),
},
},
}
)

func reconcileKeystore(params Params, configHash hash.Hash) (*keystore.Resources, error) {
if keystoreResources, err := keystore.ReconcileResources(
params.Context,
params,
&params.Logstash,
logstashv1alpha1.Namer,
NewLabels(params.Logstash),
initContainersParameters,
); err != nil {
return nil, err
} else if keystoreResources != nil {
_, _ = configHash.Write([]byte(keystoreResources.Version))
// set keystore password in init container
if env := getKeystorePass(params.Logstash); env != nil {
keystoreResources.InitContainer.Env = append(keystoreResources.InitContainer.Env, *env)
}

return keystoreResources, nil
}

return nil, nil
}

// getKeystorePass return env LOGSTASH_KEYSTORE_PASS from main container if set
func getKeystorePass(logstash logstashv1alpha1.Logstash) *corev1.EnvVar {
for _, c := range logstash.Spec.PodTemplate.Spec.Containers {
if c.Name == logstashv1alpha1.LogstashContainerName {
for _, env := range c.Env {
if env.Name == KeystorePassKey {
return &env
}
}
}
}

return nil
}
6 changes: 6 additions & 0 deletions pkg/controller/logstash/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ func buildPodTemplate(params Params, configHash hash.Hash32) (corev1.PodTemplate

ports := getDefaultContainerPorts()

if params.KeystoreResources != nil {
builder = builder.
WithVolumes(params.KeystoreResources.Volume).
WithInitContainers(params.KeystoreResources.InitContainer)
}

builder = builder.
WithResources(DefaultResources).
WithLabels(labels).
Expand Down
Loading

0 comments on commit cce77a3

Please sign in to comment.