Deploying Grafana into a Kubernetes Cluster
I recently needed to set up a new Grafana instance. Previously I've run Grafana using Docker, but this time, I decided to run it in my K8S cluster instead.
There was no particular reason for choosing to run it in Kubernetes other than that I had the cluster sat there, with spare resources and it avoided needing to make other arrangements.
Grafana have some documentation on deploying into Kubernetes, but it skips over things like provisioning storage and customising configuration (to do things such as enabling SMTP so that alert emails can be sent).
This documentation details how to deploy and configure Grafana in a Kubernetes cluster, with some guidance on how to provision some persistent storage so that changes made in Grafana survive pod restart and replacement.
Configuring Grafana
Although it's the main aim, we're not just going to stand up a Grafana instance, we also want to be able to configure it.
In this doc, we'll configure SMTP, but it's possible to take any configuration item from grafana.ini
and define it via environment variable - the variable name is of the form $GF_[section]_[name]
.
For example
[paths]
logs = "foo"
Becomes $GF_PATHS_LOGS = 'foo'
To Namespace or Not
Creating separate namespaces for separate things is best practice and the Grafana docs observe this, running
kubectl create namespace my-grafana
However, it's worth being aware that, if you're not used to doing this, you can make quite a mess if you later accidentally apply the manifests without specifying the relevant namespace. Every command you run will need to include it (or you can use something like kubectx
to switch between)
kubectl -n my-grafana get all
To avoid confusion, example calls in this doc won't specify a namespace
Storage
Grafana's Doc doesn't really go into persistent storage, so if you follow the guide to the letter you'll likely be left with an unbound or pending Persistent Volume Claim (PVC).
You need some kind of persistent storage so that you don't lose dashboards/users/alerts etc whenever a pod is restarted.
The manifest creation stage later in this doc includes an example of using NFS, but when I first set this up, I opted to create a volume using local-storage
(I was experimenting on a single node Kubernetes instance).
Note: If you're running a multi-node cluster you almost certainly don't want to do this (because it requires affinity to a single node).
Create a directory to house files
mkdir -p /home/ben/volumes/grafana
Create a manifest (I called mine grafana-storage.yml
)
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: grafana-local-pvc
spec:
storageClassName: local-storage
volumeName: "grafana-local-storage"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: grafana-local-storage
spec:
claimRef:
name: grafana-local-pvc
# If you're not using the default
# namespace you'll need to specify
# the name here
#namespace: my-grafana
capacity:
storage: 2Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /home/ben/volumes/grafana
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
# Hostname of the node that holds the storage
- bumblebee
Provision the storage by applying the manifest
kubectl apply grafana-storage.yml
Create a Manifest
With storage provisioned, we now need to define the Grafana instance.
First, we create a secret so that we don't have SMTP credentials sat in a random manifest:
kubectl create secret generic notifications-smtp \
--from-literal=user=<smtp username> \
--from-literal=password=<smtp password> \
--from-literal=host=<smtp server:port>
Then we start to create a manifest (as grafana.yml
):
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: grafana
name: grafana
spec:
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
securityContext:
fsGroup: 472
supplementalGroups:
- 0
containers:
- name: grafana
image: grafana/grafana:latest
imagePullPolicy: IfNotPresent
env:
# Enable SMTP
- name: GF_SMTP_ENABLED
value: "true"
- name: GF_SMTP_FROM_ADDRESS
value: "notifications@example.com"
- name: GF_SMTP_FROM_NAME
value: "Bens Grafana"
# Use the secret to define these values
- name: GF_SMTP_HOST
valueFrom:
secretKeyRef:
name: notifications-smtp
key: host
- name: GF_SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: notifications-smtp
key: password
- name: GF_SMTP_USER
valueFrom:
secretKeyRef:
name: notifications-smtp
key: user
ports:
- containerPort: 3000
name: http-grafana
protocol: TCP
# Have k8s monitor container health
readinessProbe:
failureThreshold: 3
httpGet:
path: /robots.txt
port: 3000
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 2
livenessProbe:
failureThreshold: 3
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
tcpSocket:
port: 3000
timeoutSeconds: 1
# Limit the resources that
# can be consumed
resources:
requests:
cpu: 250m
memory: 750Mi
# Mount our PV
volumeMounts:
- mountPath: /var/lib/grafana
name: grafana-local-pv
# Link the volume to the claim
volumes:
- name: grafana-local-pv
persistentVolumeClaim:
claimName: grafana-local-pvc
If you're intending to use a NFS share you'll want to change that last section to something like
volumes:
# If you change the name, remember to update
# the name in volumeMounts too
- name: grafana-local-pv
nfs:
server: 192.168.3.233
path: /volume1/kubernetes_grafana_data
readOnly: false
If we applied the manifest now, a Grafana pod would spin up but would be inaccessible: it needs a service definition to expose port 3000.
Add the following to the end of grafana.yml
---
apiVersion: v1
kind: Service
metadata:
name: grafana
spec:
ports:
- port: 3000
protocol: TCP
targetPort: http-grafana
selector:
app: grafana
sessionAffinity: None
type: LoadBalancer
Apply the manifest to stand Grafana up:
kubectl apply -f grafana.yml
Wait a minute or two and then verify that there's a Grafana pod running
kubectl get pods
Retrieve the service details
kubectl get service grafana
This should show you details similar to the following:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
grafana LoadBalancer 10.98.119.76 <pending> 3000:32574/TCP 128h
If you're not using a Cloud Kubernetes cluster, it's quite likely that external IP will always be in a pending state (because your network doesn't have the gubbins needed to assign one).
However, hopefully you've set up routes to your cluster range and so can now hit Grafana via the Cluster IP:
curl -v http://10.98.119.76:3000
If that works, you should be able to visit Grafana in your browser. Use admin
/admin
to log in for the first time and set a new admin password.
If you go to http://<your cluster ip>:3000/alerting/notifications
you should be able to create a Contact Point and successfully send a test email.
Although not covered here, you might also want to look at accessing Grafana via a HTTPS reverse proxy, whether thats a Nginx instance, or by using a Nginx ingress