One of the strengths of ksonnet mixin libraries is their ability to allow users to separate a Kubernetes application into several modular components.
For example, a team might split into application and logging subteams. Rather than writing a single YAML file that combines them into a single Kubernetes app, the logging team can simply write a mixin library that the application team can use to add logging to their Kubernetes application definition.
In this tutorial, we will explore how such libraries are constructed, using a mixin library for fluentd (hosted in the official mixins repository). Specifically, we see how one team writing an app using Elasticsearch can use the Fluentd mixin library to use easily configure Fluentd to tail the Elasticsearch logs and pass them Kibana to be rendered in a dashboard.
For more information about Elasticsearch and Kibana, see the Elastic
website. For fluentd
, see the Fluentd website.
If you want to build your own mixin libraries, or write ksonnet using the built-in mixins, you need to perform the following tasks. For details, see the readme.
- Install Jsonnet, version 0.9.4 or later
- Clone the ksonnet repository locally
- Install and configure the Visual Studio Code extension (optional)
- Create a test Kubernetes cluster
The idea of the application is for Elasticsearch to emit logs to standard out, and for Fluentd to tail those logs and send them to Kibana for rendering.
In Kubernetes, accessing the Pod
logs involves:
- Giving the Fluentd container permissions to access the
Pod
logs, and - Appending volume mounts that contain the
Pod
logs, to the Fluentd container, so that it can access them.
We'll walk through the key parts of the files in example in detail, but at a high level this implementation is broken up as:
-
A
DaemonSet
that causes Fluentd to run once on every machine, so that it can tailPod
logs for Elasticsearch running anywhere in the cluster.On its own, this
DaemonSet
only contains the core Fluentd application definition. For example, it has no permissions to access (e.g.)Pod
Logs, or the volume mounts required to access them. -
A separate mixin that defines the
VolumeMounts
andVolumes
that theDaemonSet
requires to access thePod
Logs. -
A separate mixin that configures the access permissions for the
DaemonSet
-
The RBAC objects that the cluster administrator must send to the cluster so that the
ServiceAccount
associated with Fluentd can be granted permission to obtain thePod
logs.
The power of this approach lies in its separation of concerns: an
application developer can define the DaemonSet
, while a cluster
admin can define the access permissions that this or any other
DaemonSet
might require. The DaemonSet
or the access permissions
can be modified as needed without requiring a complete cluster
reconfiguration. Indeed, as the DaemonSet
mixin demonstrates, the
details of the DaemonSet
(in this case, the Volumes
and
VolumeMounts
) can also be adjusted without having to touch the base
DaemonSet
definition.
Let's look at how we can decouple the pieces of a complete Fluentd configuration, so that your logging team, for example, can write just the core of a Fluentd DaemonSet, and then write a ksonnet library that lets you customize key details of the configuration as needed.
fluentd-es-ds.jsonnet
defines a basic DaemonSet, and then adds access permissions to it.
// daemonset
local ds =
// base daemonset
fluentd.app.daemonSetBuilder.new(config) +
// add configuration for access to pod logs
fluentd.app.daemonSetBuilder.configureForPodLogs(config);
// create access permissions for pod logs
local rbacObjs = fluentd.app.admin.rbacForPodLogs(config);
Note that our base DaemonSet can't do anything. It doesn't know where the pod logs that it needs are -- it needs Volumes and VolumeMounts to provide this information. It also needs access permissions, provided with RBAC. So we add these items separately. Let's look more closely at the advantages of this approach.
In fluentd.libsonnet
, we define the daemonSet
mixin. Here is where
we start to see the real power of ksonnet mixins at work. This
mixin specifies the VolumeMounts and Volumes that Fluentd requires
separately from the DaemonSet definition itself. This approach lets us
decouple application definitions from deployment details.
Note particularly in the following snippet the containerSelector
parameter to addHostMountedPodLogs
. We pass this function to
ds.mapContainers
to iterate over our containers (in this case, our
Fluentd containers) and add the VolumeMounts that they need. (The
details of the pod logs have also been abstracted away to their own
function.)
mixin:: {
daemonSet:: {
// Takes two volume names and produces a
// mixin that mounts the Kubernetes pod logs into a set of
// containers specified by `containerSelector`.
addHostMountedPodLogs(
varlogName, podlogsName, containerSelector=function(c) true
)::
local podLogs = $.parts.podLogs(varlogName, podlogsName);
// Add volume to DaemonSet.
ds.mixin.spec.template.spec.volumes([
podLogs.varLogVolume,
podLogs.podLogVolume,
]) +
// Iterate over a specified set of containers to add the VolumeMounts
ds.mapContainers(
function (c)
if containerSelector(c)
then
c + container.volumeMounts([
podLogs.varLogMount,
podLogs.podLogMount,
])
else c),
},
},
The daemonSetBuilder
that we used to create the DaemonSet calls our
daemonSet
mixin, and also defines the configureForPodLogs
function
that the DaemonSet needs. But the DaemonSet itself, from our first
code snippet, doesn't need to know any of these details:
daemonSetBuilder:: {
new(config):: {
toArray():: [self.daemonSet],
daemonSet:: $.parts.daemonSet(config.daemonSet.name, config.container.name, config.container.tag, config.namespace)
},
// access configuration
configureForPodLogs(
config,
varlogVolName="varlog",
podLogsVolName="varlibdockercontainers",
)::
{} + {
daemonSet+::
$.mixin.daemonSet.addHostMountedPodLogs(
varlogVolName,
podLogsVolName,
$.util.containerNameInSet(config.container.name)) +
// RBAC and service account
ds.mixin.spec.template.spec.serviceAccountName(config.rbac.accountName)
},
},
In the previous snippet, we notice that we're specifying a Service Account, and RBAC is involved. It's time to define our RBAC objects so that our Fluentd access permissions mean something.
We define RBAC objects separately so that they can be managed independently of the rest of the cluster configuration. This approach lets cluster admins and application developers work independently. Your cluster admins can determine and define access permissions that can be applied to application configurations with a few lines of code.
Defining access permissions in Kubernetes requires definition of the
RBAC objects that are encapsulated in this definition (from
fluentd.libsonnet
).
admin:: {
rbacForPodLogs(config)::
$.parts.rbac(config.rbac.accountName, config.namespace),
},
Let's unpack this snippet.
fluentd.libsonnet
also defines all the required RBAC objects. Note
especially that we abstract the attributes of the Service Account
separately and assign their values in a separate config
object. This
approach lets us make sure that the correct Service Account is
appropriately associated with all required objects.
rbac(name, namespace)::
local metadata = svcAccount.mixin.metadata.name(name) +
svcAccount.mixin.metadata.namespace(namespace);
local hcServiceAccount = svcAccount.new() +
metadata;
local hcClusterRole =
clRole.new() +
metadata +
clRole.rules(
rule.new() +
rule.apiGroups("*") +
rule.resources(["pods", "nodes"]) +
rule.verbs(["list", "watch"])
);
local hcClusterRoleBinding =
clRoleBinding.new() +
metadata +
clRoleBinding.mixin.roleRef.apiGroup("rbac.authorization.k8s.io") +
clRoleBinding.mixin.roleRef.name(name) +
clRoleBinding.mixin.roleRef.mixinInstance({kind: "ClusterRole"}) +
clRoleBinding.subjects(
subject.new() +
subject.name(name) +
subject.namespace(namespace)
{kind: "ServiceAccount"}
);
In fluentd-es-ds.jsonnet
we define our config thus:
local config = {
namespace:: "elasticsearch",
container:: {
name:: "fluentd-es",
tag:: "1.22",
},
daemonSet:: {
name:: "fluentd-es-v1.22",
},
rbac:: {
accountName:: "fluentd-serviceaccount"
},
};
The relevant fields here are namespace
and AccountName
, which we
pass as the arguments that our RBAC snippet needs when it calls the
rbac
function.
Here's where we started, with our simple DaemonSet, its pod logging, and its access permissions. But now you've seen what's going on underneath -- not just how the functions for adding pod logs and permissions are clearly separated, but how we can customize them as needed without having to rewrite the entire configuration.
// daemonset
local ds =
// base daemonset
fluentd.app.daemonSetBuilder.new(config) +
// add configuration for access to pod logs
fluentd.app.daemonSetBuilder.configureForPodLogs(config);
// create access permissions for pod logs
local rbacObjs = fluentd.app.admin.rbacForPodLogs(config);
The GitHub example directory also includes the generated JSON files. Examine them to help understand the details of how ksonnet's decomposition and abstraction are compiled into complete configurations.
As you start to write your own custom mixins, look also at how we break down the basic ksonnet imports into smaller component objects for easier manipulation.
And feel free to contribute your own examples to our mixins repository!