Category Archives: Kubernetes

Kubernetes Pipeline

Let’s explain an easy way to build an integration pipeline (CI) on Minikube.

Launch Minikube

If you don’t have Minikube running on your system,

$ minikube start --memory 4000 --cpus 2

Wait for a few minutes, you’ll see something like.

Starting local Kubernetes v1.10.0 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.
Loading cached images from config file.

Installing Helm

Helm is The Kubernetes Package Manager, it helps you to deploy services into Kubernetes.

$ wget https://storage.googleapis.com/kubernetes-helm/helm-v2.10.0-linux-amd64.tar.gz -O helm.tar.gz
$ tar zxf helm.tar.gz
$ sudo cp linux-amd64/helm /usr/local/bin/helm
$ sudo chmod +x /usr/local/bin/helm

Applying the RBAC policy

$ kubectl create -f https://raw.githubusercontent.com/nordri/kubernetes-experiments/master/Pipeline/ServiceAccount.yaml

and then launch helm.

helm init --service-account tiller

Checking

$ helm version
Client: &version.Version{SemVer:"v2.10.0", GitCommit:"9ad53aac42165a5fadc6c87be0dea6b115f93090", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.10.0", GitCommit:"9ad53aac42165a5fadc6c87be0dea6b115f93090", GitTreeState:"clean"}

Deploying Jenkins

I’m using a custom values file for this chart. What I’m adjusting is:

AdminPassword: set to admin1234
ServiceType: set to NodePort (because is Minikube)
In plugins:
– kubernetes:1.2
– workflow-aggregator:2.5
– workflow-job:2.17
– credentials-binding:1.15
– git:3.7.0

And then the deployment:

$ helm install --name jenkins -f jenkins-helm-values.yaml stable/jenkins

After a few minutes we could be able to access Jenkins with:

$ minikube service jenkins

Configuring Jenkins

First, set the credentials to access Docker Hub where we’ll push the Docker images. The only field you must keep is ID because is needed by the pipeline in a next step. Fill it with your information:

Back to Jenkins main screen, add a new item type Pipeline

And finally, configure the pipeline in the Pipeline section:

Save the changes and click on Build now

And that’s it!

The pipeline

Let’s deep into the pipeline

The head

The pipeline starts setting the worker id so the pod has different label on each execution.

Follow the pod definition where we can define the containers who will run inside the pod. For this example we’ll need:

  1. maven
  2. docker
  3. mysql, this one with environment variables
  4. java, also with environment variables

Then the volumes, we need the docker sock in order to run docker in docker and a folder to save the artefacts downloaded from the Internet (it’s a Maven project!) between the executions. Saving time and bandwidth.

Cloning the repo…

What we do here is clean the workspace and clone the repository. It a SpringBoot application with MySQL.

Building…

We build the package using maven container.

Testing…

In this stage we launch our app inside Java container and after 30 seconds we check if it online, a simple smoky test. We save the return value in RES to decide if it’s ok or not. If not, finish with fail. As we defined all the containers at the beginning there’s a MySQL running inside the pod.

Building & Uploading Docker images…

If the testing stage went OK, we can push it to Docker Hub. To set the tag we use the commit ID cut to eight characters. To login into Docker Hub we use the withCredentials who takes a credential by id and fill the environment variables.

References

Set Up a Jenkins CI/CD Pipeline with Kubernetes

Repository

GitHub

Kubernetes Network Policy

On this post, I will show how you can isolate services within the same namespace in Kubernetes.

Why would you want to do that? Think of this as if you want to test your app behind a firewall.

To achive this, Kubernetes provides Network Policy, it allows us to protect who can connect to a service. So, the first step will be deny all traffic in our namespace:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
  - Ingress

Now, every service you deploy won’t be reachable within the cluster.

Let’s deploy now an Apache Server.

---
kind: Service
apiVersion: v1
metadata:
  name: apache1
  labels:
    app: web1
spec:
  selector:
    app: web1
  ports:
  - protocol: TCP
    port: 80
    name: http
---
apiVersion: apps/v1 # for versions before 1.8.0 use apps/v1beta2
kind: Deployment
metadata:
  name: apache1
  labels:
    app: web1
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: web1
  replicas: 1 
  template: 
    metadata:
      labels:
        app: web1
    spec:
      containers:
      - name: apache-container
        image: httpd:2.4
        ports:
        - containerPort: 80

And a simple container to test the connection:

---
apiVersion: apps/v1 # for versions before 1.8.0 use apps/v1beta1
kind: Deployment
metadata:
  name: ubuntu1
  labels:
    allow-access: "true"
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      allow-access: "true"
  replicas: 1 
  template: 
    metadata:
      labels:
        allow-access: "true"
    spec:
      containers:
      - name: nordri-container
        image: nordri/nordri-dev-tools
        command: ["/bin/sleep"]
        args: ["3600"]

Here, you can see a label allow-access: “true” we’ll use that to grant access to the service.

And, finally, the Network Policy.

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: net1
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: web1
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          allow-web1: "true"
    ports:
    - protocol: TCP
      port: 80

As you can see, we can protect the Service web1 allowing the access only from those pods with label allow-access: “true”.

At this point you have something like that

NAME                         READY     STATUS              RESTARTS   AGE
po/apache1-5565b647c-dt6kk   1/1       Running   0          38s
po/ubuntu1-c8cffc57b-q62tw   1/1       Running   0          38s

NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
svc/apache1      ClusterIP   10.111.18.130           80/TCP    39s
svc/kubernetes   ClusterIP   10.96.0.1               443/TCP   7m

NAME             DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/apache1   1         1         1            0           38s
deploy/ubuntu1   1         1         1            0           38s

NAME                   POD-SELECTOR   AGE
netpol/default-deny             59s
netpol/deny-metadata            6m
netpol/net1            app=web1       38s

And you can test from the container:

kubectl exec ubuntu1-c8cffc57b-q62tw -- curl apache1
<html><body><h1>It works!</h1></body></html>

But, from any other pod

kubectl run nordri-dev-tools --rm -ti --image=nordri/nordri-dev-tools /bin/bash
If you don't see a command prompt, try pressing enter.
root@nordri-dev-tools-766cc58546-kmhbf:/# 
root@nordri-dev-tools-766cc58546-kmhbf:/# curl apache1
curl: (7) Failed to connect to apache1 port 80: Connection timed out
root@nordri-dev-tools-766cc58546-kmhbf:/# 

Cool, isn’t it?

Let’s do something more complex and interesting. Check out this scenario:

What we want is:

Master  -> apache1 OK
Master  -> apache2 OK
Ubuntu1 -> apache1 OK
Ubuntu2 -> apache2 OK
Ubuntu1 -> apache2 Time out
Ubuntu2 -> apache1 Time out

So, clone this GitHub repository

git clone https://github.com/nordri/kubernetes-experiments

And run…

cd NetworkPolicy
./up.sh

This script will deploy all the components of our experiment. Now, there’s something like this in your Kubernetes:

NAME                          READY     STATUS    RESTARTS   AGE
po/apache1-5565b647c-fxcpc    1/1       Running   0          1m
po/apache2-587775d7bd-pjg98   1/1       Running   0          1m
po/master-6df9c89f5b-pqtl2    1/1       Running   0          1m
po/ubuntu1-588f5bdbfc-qkh8t   1/1       Running   0          1m
po/ubuntu2-f689cc5d-ncbzh     1/1       Running   0          1m

NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
svc/apache1      ClusterIP   10.110.39.59            80/TCP    1m
svc/apache2      ClusterIP   10.109.61.240           80/TCP    1m
svc/kubernetes   ClusterIP   10.96.0.1               443/TCP   23m

NAME             DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/apache1   1         1         1            1           1m
deploy/apache2   1         1         1            1           1m
deploy/master    1         1         1            1           1m
deploy/ubuntu1   1         1         1            1           1m
deploy/ubuntu2   1         1         1            1           1m

NAME                   POD-SELECTOR   AGE
netpol/default-deny             1m
netpol/deny-metadata            22m
netpol/etm-net2        app=web2       1m
netpol/net1            app=web1       1m

And we can check that apache1 and apache2 only can be reached from master and the container sharing the Network policy.

To clean the house just run:

cd NetworkPolicy
./down.sh

References:

OpenStack & Kubernetes: LBaaS y Self Service Provisioning

Ya que tenemos desplegado Kubernetes en OpenStack vamos a aprovechar las capacidades de este para desplegar nuestras aplicaciones.

Configurar Kubernetes para utilizar OpenStack

El primer paso que tenemos que dar es configurar nuestras credenciales de OpenStack en Kubernetes. Para ello editamos el fichero /etc/kubernetes/cloud_config y añadimos las siguientes líneas:

[Global]
auth-url=http://AUTH_URL:5000/v3
username=demo
password=s3cr3t
region=RegionOne
tenant-id=d245f207e6fc4e56b5a1d23e0716ad70
domain-name=Default

De esta forma el API de Kubernetes puede comunicar con el API de OpenStack y configurar los servicios.

El campo domain-name solo es necesario si usamos API v3 de OpenStack, como indica la AUTH_URL.

Ahora debemos cambiar añadir lo siguiente:

KUBE_API_ARGS=[...] --cloud-provider=openstack --cloud-config=/etc/kubernetes/cloud_config

al arranque de nuestro Kubernetes apiserver.

Igualmente en /etc/kubernetes/manifests/kube-controller-manager.yaml:

- [...]
- --cloud_config=/etc/sysconfig/kube_openstack_config
- --cloud_provider=openstack

Aquí la cosa se complica y depende de tu instalación. Si tienes el binario en el sistema solo tienes que ajustar en /etc/kubernetes/* pero si estás usando contenedores, deberás volver a construir los contenedores y añadir estos parámetros.

Utilizar el LBaaS de OpenStack en Kubernetes

Para habilitar el balanceador debemos añadir los siguientes datos en nuestro fichero de cloud_config:

  1. La subred donde se va a construir el balanceador.
  2. Debemos indicar que maneje los security groups.
  3. El security group que se debe aplicar al puerto del balanceador. Es el vip_port_id anterior.
  4. Y por último la id de la red de la que tomará la IP flotante.

Deberiamos tener algo así:

[LoadBalancer]
lb-version=v2
subnet-id=82f277d7-1b29-49b3-94ee-40424c4e36d9
manage-security-groups=true
node-security-group=b063453a-41fb-47e5-ac3e-fbe270fc2232
floating-network-id=ccee1f8b-c813-4a69-a072-ee1e10aba677

¿Qué tiene que pasar? Ahora, cuando en el service pongamos LoadBalancer Kubernetes utilizará las credenciales que le hemos proporcionado y construirá un balanceador completo para la aplicación. De la red que le hemos proporcionado cogerá un IP flotante libre. Esta es la IP a la que debemos atacar cuando queramos utilizar el servicio recién publicado.

Self Service Provisioning

Para que nuestras aplicaciones conserven los datos que generen en su tiempo de vida es necesario proporcionarles volúmenes de persistencia. Para ello comenzamos creando un nuevo objeto de Kubernetes llamado StorageClass donde vamos a definir como debe pedir el volumen a OpenStack y como vamos a llamarlo desde nuestra aplicación. Podemos definir tantos como sistemas de aprovisionamiento tengamos. El StorageClass tiene esta pinta:

kind: StorageClass
apiVersion: storage.k8s.io/v1beta1
metadata:
  name: gold #-> este es el nombre con el que nos referiremos a el cuando hagamos el claim
provisioner: kubernetes.io/cinder
parameters:
  type: kube-storage #-> Esto hay que definirlo en openstack
  availability: nova

Como vemos los dos valores que tenemos que definir son name y type. Entonces, cuando queramos que nuestra aplicación haga uso de esta persistencia dinámica usaremos algo como esto:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: claim1
  annotations:
    volume.beta.kubernetes.io/storage-class: gold #-> Este es el nombre que le hemos dado
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 30Gi

Antes había que hacer uso de PersistentVolume pero está siendo reemplazado por este nuevo objeto que creará el volumen dinámicamente.

Referencias

Empaquetando Aplicaciones para Kubernetes

En la entrada anterior hemos visto como utilizar las herramientas que nos proporciona Kubernetes para construir nuestra aplicación. Ahora vamos a ver lo fácil que es construir un paquete con toda la información que va a necesitar nuestra aplicación para funcionar.

El gestor de paquetes de Kubernetes

El gestor de paquete de Kubernetes se llama Helm. También es el nombre del comando.

Para instalar Helm simplemente nos descargamos el binario y lo colocamos en el PATH.

$ wget http://storage.googleapis.com/kubernetes-helm/helm-v2.1.3-linux-amd64.tar.gz
$ mv linux-amd64/helm /usr/local/bin/helm

Podemos ver que está instalado con:

$ helm version
Client: &version.Version{SemVer:"v2.1.2", GitCommit:"58e545f47002e36ca71ac5d1f7a987b56e1937b3", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.1.2", GitCommit:"58e545f47002e36ca71ac5d1f7a987b56e1937b3", GitTreeState:"clean"}

Helm cuenta con otro componente que se llama Tiller, es un pod que actua como un agente dentro de Kubernetes. La forma más rápida de desplegar el agente es así:

$ helm init

Esto tomará los valores por defecto para conectar con el clúster y desplegar el agente. Vemos el agente desplegado como pod

$ kubectl --namespace=kube-system get pods --selector='name=tiller'
NAME                             READY     STATUS    RESTARTS   AGE
tiller-deploy-3161388333-0lqd4   1/1       Running   8          6d

Estructura de un paquete para Kubernetes/Helm

Al igual que otros paquetes como los deb de Debian los paquetes de Helm se llaman Charts y tienen la siguiente estructura:

wordpress/
  Chart.yaml          # A YAML file containing information about the chart
  LICENSE             # OPTIONAL: A plain text file containing the license for the chart
  README.md           # OPTIONAL: A human-readable README file
  values.yaml         # The default configuration values for this chart
  charts/             # OPTIONAL: A directory containing any charts upon which this chart depends.
  templates/          # OPTIONAL: A directory of templates that, when combined with values,
                      # will generate valid Kubernetes manifest files.
  templates/NOTES.txt # OPTIONAL: A plain text file containing short usage notes

Todos nos interesan ahora mismo excepto el de charts/ porque nuestro Chart (de wordpress) no depende de ningún otro. Vamos a ir viendo cada uno al tiempo que vemos como lo hemos adaptado a nuestros yaml para wordpress

Nuestro Chart de WordPress

Chart.yaml

name: wp-helm
description: An example of Helm and WordPress
version: 0.1.0
keywords:
  - http
  - wordpress
  - www
  - web
  - cms
home: "https://www.wordpress.org"
sources:
  - "https://github.com/nordri/helm-wordpress"
maintainers:
  - name: nordri
    email: nordri@gmail.com
engine: gotpl

No hay mucho que contar, está bastante claro. La opción engine hace referencia al parseador que usaremos para las plantillas, la opción por defecto si no se indica es gotpl que es el que usa Golang, como Jinja2 para Python.

LICENSE

Es el texto de la Licencia Apache 2

README.md

La información relativa a nuestro Chart, como funciona y demás

Un ejemplo de WordPress con Helm y Kubernetes.........

values.yaml

Este fichero contiene los datos de nuestra instalación. Como por ejemplo el nombre del sitio. Puertos a usar, usuarios y contraseñas.

image: wordpress
db_server: 192.168.1.43
root_db_password: root01
nfs_path: srvnfs
nfs_server: 192.168.1.43
pv_size: 20Gi
pvc_size: 2Gi
hostname: wordpress
resources:
  requests:
    memory: 128Mi
    cpu: 500m

Para este ejemplo hemos introducido dos nuevos parámetros que van a controlar el uso de los recursos por parte de nuestra aplicación. La memoria que hemos establecido a 128Mi y la cpu a 500m (500 milicores)

charts

Si usáramos dependencias de otros charts los incluiríamos en este directorio. Seguiría la misma estructura de directorios del padre, por ejemplo si nuestro chart depende del chart Apache y MySQL sería:

wordpress:
  Chart.yaml
  requirements.yaml
  # ...
  charts/
    apache/
      Chart.yaml
      # ...
    mysql/
      Chart.yaml
      # ...

templates

Aquí es donde vamos a definir cada uno de los componentes que conforman nuestra aplicación. Cada fichero yaml que utilizamos en la entrada anterior aparece aquí parametrizado para que coja los valores del fichero de values.yaml. Por no alargar mucho la entrada vamos a ver los detalles más significativos:

Hay dos ficheros clave que son _helpers.tpl y NOTES.txt. El primero es donde definimos funciones que serán ejecutadas por el parseador y el segundo es el mensaje que presentaremos cuando la aplicación se instale, como la URL de acceso.

Una vez tenemos las funciones definidas podemos hacer uso de ellas dentro de nuestras plantillas. En nuestro caso usamos la función template con el parámetro “fullname” para obtener el nombre de la instalación. Esto es porque, al igual que con Docker, cada instalación recibe un nombre aleatorio dentro del clúster, así podemos definir por ejemplo para nuestro servicio la siguiente línea:

name: {{template "fullname" .}}-svc

Ese será el nombre del servicio y lo usaremos para relacionar cada componente de la aplicación dentro del cluster.

Lo siguiente será sacar valores del fichero values.yaml y usaríamos la siguiente sintaxis:

# Obtener el nombre del host que le pasaremos a ingress
- host: {{ .Values.hostname }}

También podemos tener en la plantilla un nombre por defecto en caso que no se rellene ese valor en values.yaml

- host: {{ default "localhost" .Values.hostname }}

Los valores pueden hacer uso de pipelines al estilo Bash por ejemplo para cifrar una password y entrecomillarla. Lo vemos en el fichero de mysql-secrets.yaml.

mysql-root-password: {{.Values.root_db_password | b64enc | quote}}

Vemos que la contraseña está en plano en el fichero de values, la tomamos, la pasamos a base64 y la ponemos entre comillas.

Si queremos incluir trozos de values sin tener que referenciarlos uno a uno podemos hacer uso de toYaml que es una función para llamar a plantillas, tenemos que definir una plantilla dentro de values, nosotros hemos llamado resources y luego pasarla por la función de indentado, así si queremos incluir los resources que puede hacer uso el pod hacemos así:

{{ toYaml .Values.resources | indent 10 }}

Y tomará de values el trozo de plantilla que corresponda con resources y lo indentará 10 posiciones.

También se usa una lista de variables ya definidas que podemos consultar aquí

Instalando el paquete

Una vez tenemos todo instalamos el paquete. Si vemos la ayuda de Helm tenemos varias formas de instalar el paquete:

[...]
1. By chart reference: helm install stable/mariadb
2. By path to a packaged chart: helm install ./nginx-1.2.3.tgz
3. By path to an unpacked chart directory: helm install ./nginx
4. By absolute URL: helm install https://example.com/charts/nginx-1.2.3.tgz
[...]

Si lo instalamos usando la opción 3 veremos algo parecido a esto:

NAME:   winsome-tortoise
LAST DEPLOYED: Thu Dec 29 12:19:26 2016
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Secret
NAME                                TYPE      DATA      AGE
winsome-tortoise-wordpress-secret   Opaque    1         1s

==> v1/PersistentVolume
NAME                            CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                                    REASON    AGE
winsome-tortoise-wordpress-pv   20Gi       RWX           Retain          Bound     default/winsome-tortoise-wordpress-pvc             1s

==> v1/Service
NAME                             CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
winsome-tortoise-wordpress-svc   10.111.214.123          80:32590/TCP   1s

==> extensions/Deployment
NAME                         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
winsome-tortoise-wordpress   1         1         1            0           1s

==> extensions/Ingress
NAME                             HOSTS       ADDRESS   PORTS     AGE
winsome-tortoise-wordpress-ing   wordpress             80        1s

==> v1/PersistentVolumeClaim
NAME                             STATUS    VOLUME                          CAPACITY   ACCESSMODES   AGE
winsome-tortoise-wordpress-pvc   Bound     winsome-tortoise-wordpress-pv   20Gi       RWX           1s


NOTES:
Los datos de acceso son los siguientes:

Host: wordpress

Donde vemos un resumen de los componentes que han sido lanzados y la información que nos da el fichero NOTES.txt. Vemos las aplicaciones instaladas a través de helm, asi

$ helm list
NAME               REVISION    UPDATED                     STATUS      CHART        
winsome-tortoise    1           Thu Dec 29 12:19:26 2016    DEPLOYED    wp-helm-0.1.0

Si quisiéramos eliminar la aplicación hariamos:

$ helm delete winsome-tortoise

Repositorio

Aquí el repositorio en GitHub: aquí

WordPress Sobre Kubernetes

En la siguiente entrada vamos a explicar como podemos desplegar una legacy application, como WordPress, en un entorno de cloud como es Kubernetes.

Kubernetes se basan en una unidad mínima funcional llamada Pod, un Pod es uno o más contenedores, los contenedores pueden ser Docker o más recientemente, LXC. ¿Qué ocurre? que los contenedores son volátiles. Es decir, están condenados a desaparecer y volver a recrearse para siempre. Así, ¿qué pasa con la información que se va creando dentro de los Pods? Desaparece. Para evitar esta situación introducimos el concepto de persistencia. Básicamente consiste en configurarar el contenedor para que la información sensible que se va generando se guarde en un sitio inmune a la muerte y renacimiento de los contenedores. Para el caso que vamos a explicar aquí trataremos el sistema de ficheros y la base de datos. Por un lado, necesitamos que el WordPress siempre tenga acceso a su código PHP (aunque lo ideal sería conservar simplemente el wp-content) y por otro la base de datos, que es donde se guarda las entradas y demás.

La base de datos

Vamos a crear una máquina que funcionara como servidor de base de datos de MariaDB, configuramos para que acepte conexiones remotas.

Volvemos a nuestro master de Kubernetes y vamos a crear un secret

Un secret es un trozo de configuración para nuestras aplicaciones que introducimos en el fichero YAML. De está forma podemos pasar credenciales e información sensible en nuestro clúster sin que esté expuesta. En nuestro ejemplo vamos a crear un secret para pasar las credenciales de nuestra base de datos.

Primero codificamos la contraseña en base64, para este ejemplo, la contraseña de root es root01

$ echo -n root01 | base64
cm9vdDAx

Y escribimos el contenido el fichero de secretes

apiVersion: v1  
kind: Secret  
metadata:  
  name: mysql-secrets
type: Opaque  
data:  
  mysql-root-password: cm9vdDAx 

Añadimos esta configuración a Kubernetes

$ kubectl create -f mysql-secrets.yaml
secret "mysql-secrets" created

Vemos los secrets haciendo:

kubectl get secrets
NAME                  TYPE                                  DATA      AGE
default-token-thlxa   kubernetes.io/service-account-token   3         1d
mysql-secrets         Opaque                                3         23s

Ahora, cada vez que necesitemos de una aplicación que haga uso de la base de datos, podemos pasar las credenciales de root utilizando este endpoint.

Persistencia de Sistema de Ficheros

Para este ejemplo hemos montado un servidor NFS que ofrecemos a la red de los nodos de Kubernetes, así en nuestro fichero /etc/exports tendremos algo así:

/srvnfs    192.168.1.0/24(rw)

Ahora tenemos que presentar este volumen a Kubernetes, para ello hacemos uso de dos nuevos recursos, Persistent Volume y Persistent Volume Claim. La definición de la documentación de Kubernetes es muy buena, así que aquí lo explicaremos rápidamente:

  • Persistent Volume Es el pastel que entregamos al clúster, entero, sin partir, en bruto, tal cual.
  • Persistent Volume Claim Es cada porción de pastel que vamos a partir con cada pod que necesite persistencia.

Para crear estos recursos tenemos los siguientes YAML.

 
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-wp-content
spec:
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: /srvnfs
    server: 192.168.1.43

Y para el claim

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-wp-content
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 2Gi

Nota: Hay que instalar nfs-client en todos los nodos.

Se pueden usar otros drivers para persistencia como Ceph o Gluster, el procedimiento es el mismo.

Los creamos:

$ kubectl create -f pv-persistent.yaml
persistentvolume "pv-wp-content" created
$ kubectl create -f pvc-persistent.yaml
persistentvolumeclaim "pvc-wp-content" created

Lo verificamos:

$ kubectl get pv,pvc
NAME               CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS    CLAIM                    REASON    AGE
pv/pv-wp-content   20Gi       RWX           Retain          Bound     default/pvc-wp-content             3m
NAME                 STATUS    VOLUME          CAPACITY   ACCESSMODES   AGE
pvc/pvc-wp-content   Bound     pv-wp-content   20Gi       RWX           2m

Crear un deployment para WordPress

Ahora que está todo listo para que nuestro WordPress pueda desplegarse vamos a escribir el YAML para hacer uso de estos recursos. Partimos del ejemplo que nos da el propio Kubernetes aquí pero adaptado al clúster que nos hemos construido:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: wordpress-deployment
  labels:
    app: wordpress
spec:
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
      - image: wordpress
        name: wordpress
        env:
          - name: WORDPRESS_DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: mysql-secrets
                key: mysql-root-password
          - name: WORDPRESS_DB_HOST
            value: 192.168.1.44
        ports:
        - containerPort: 80
          name: wordpress
        volumeMounts:
        - name: wordpress-persistent-storage
          mountPath: /var/www/html
      volumes:
        - name: wordpress-persistent-storage
          persistentVolumeClaim:
            claimName: pvc-wp-content

Vemos como le pasamos la contraseña de root en una variable de entorno, pero se carga a través de un secret, de forma que no está expuesta a simple vista. También le indicamos donde va a montar /var/www/html para escribir el código PHP de WordPress.

Ahora simplemente exponemos el servicio, recordando usar NodePort.

apiVersion: v1
kind: Service
metadata:
  name: wordpress-svc
  labels:
    app: wordpress
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: wordpress
    tier: frontend
  type: NodePort

Y construimos el ingress.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
 name: wordpress-ingress
spec:
 rules:
   - host: wordpress
     http:
       paths:
         - path: /
           backend:
             serviceName: wordpress-svc
             servicePort: 80

Podemos comprobar que funciona:

$ curl -I --resolve wordpress:80:192.168.1.41 http://wordpress
HTTP/1.1 302 Found
Cache-Control: no-cache, must-revalidate, max-age=0
Content-Type: text/html; charset=UTF-8
Date: Sat, 10 Dec 2016 11:43:28 GMT
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Location: http://wordpress/wp-admin/install.php
Server: Apache/2.4.10 (Debian)
X-Powered-By: PHP/5.6.28

Conclusiones

  1. Hemos montado una legacy app sobre kuberntes teniendo en cuenta la persistencia de la información.
  2. Hay que tener en cuenta que este ejemplo coloca todos los ficheros de wordpress en el directorio compartido, por lo que si desplegamos otro sobrescribe lo anterior, si queremos tener varios WP corriendo en este entorno tendremos que configurara para que cada uno utilice una carpeta.
  3. También como dije al principio lo ideal es poner en persistencia el directorio de wp-contents que es el que conserva los datos. Esto implica cambiar el Docker y complica el ejemplo.
  4. El traefik a veces se queda pillado, de momento lo que hago es destruir el controlador para que el deplyment lo cree de nuevo.
  5. Si el traefik cambia de nodo hay que cambiar los dns para que apunten a la IP nueva. Puedes usar tier para crear afinidad con el nodo, de forma que traefik siempre funcione en el mismo nodo.
  6. También puedes tener un traefik por nodo para hacer balanceo o HA.