Kubernetes: First Deploy

Buenas, después de un parón sin escribir mucho, vuelvo a las andadas. La intención es ir sacando una serie de posts con diferentes temáticas y modalidades de Kubernetes. Desde un despliegue simple hasta como gestionar una integración continua. Para empezar, vamos a crear unos YAML que desplieguen un stack de WordPress básico. Este constará de; un Nginx, un PHP-FPM y un MySQL. La idea, es utilizar diferentes opciones para que se pueda ver las diferencias entre ellas y sea lo mas completo posible.

Todo esto, lo levantaremos sobre un Minikube. Se puede ver como instalarlo en el siguiente post.

Empezamos

Una vez desplegado Minikube, vamos a empezar a declarar los YAML que vamos a necesitar. Por comodidad, la nomenclatura de los ficheros empezarán por un número que tendrá que ver con el servicio, seguido del nombre del servicio y el recurso que vamos a utilizar.
Ej: 11-nginx-configmap.yaml

Los YAML los tengo definidos ya y subidos a github:
https://github.com/ichasco/Kubernetes-WP-test
Lo que iremos haciendo, es ir analizándolos la sintaxis de estos, uno a uno.

YAMLs transversales

Hay YAMLs que no están directamente asociados a un servicio, como pueden ser: los de namespace, roles, PDB… En este caso, solo vamos a utilizar uno, que será el de namespace. En este caso es el que tiene nombre: 00-namespace.yaml
En este fichero, vamos a definir el namespace que vamos a crear. Es recomendable crear un namespace nuevo para separar las aplicaciones. En este caso va a ser un namespace de producción

---
apiVersion: v1
kind: Namespace
metadata:
  name: production

Nginx

Para el servicio de Nginx, vamos a utilizar un configmap, un service y un deployment. A continuación explico que es cada cosa.

Configmap

En este caso, el archivo de configmap se llamará: 11-nginx-configmap.yaml. En este fichero, definiremos los ficheros de configuración que queramos que tenga Nginx, luego estos se invocarán desde el fichero de deployment.
Hay varios modos aparte de este, como crear la imagen de Docker con la configuración incluida. Yo no soy muy partidario de esto, ya que las imágenes de Docker deberían de ser lo mas estándar posibles.
En este caso, vamos a declarar los siguientes ficheros:

  • nginx.conf
  • virtualhost.conf
  • security.conf
  • cache.conf

El formato sería el siguiente:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx
  namespace: production
  labels:
    app: nginx
data:
  nginx.conf: |
  ...
  virtualhost.conf: |
  ...
  security.conf: |
  ...
  cache.conf: |
  ...

Service

Ahora vamos a definir el servicio. En este caso, se va a llamar 13-nginx-service.yaml. Esto es simplemente definir por que puerto se van a realizar las conexiones y que tipo de servicio queremos. En este caso va a ser NodePort porque nos vamos a conectar desde fuera del cluster. En un entorno productivo esto será de tipo ingress o loadblancer. O generar otro ingress que sea el que gestione los accesos.
Mas info sobre los servicios:
https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0

---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: production
spec:
  selector:
    app: nginx
  type: NodePort
  ports:
  - name: nginx-port
    port: 80
    targetPort: 80
    nodePort: 30036
    protocol: TCP

Deployment

Los deployment, como su propio nombre indica se utilizan para desplegar PODs. Por norma general, los deployment se utilizan para PODs stateless y los statefulset para stateful. Esto lo veremos también en el servicio de MySQL. En este caso, el YAML se llamará 14-nginx-deployment.yaml. En este, se declarará toda la lógica y comportamiento del POD:

  • Estrategia de despliegue: Rolling Update (Es lo mismo que Ramped)
  • Resources: Se establecen los limites de CPU y RAM. En el CPU 1000m equivale a 1 core. Por lo que 100m equivale a un 10% de 1 core.
    • request: Los recursos que se necesitan para levantar el container.
    • limits: El limite de recursos que puede coger el container.
  • livenessProbe: healthcheck de que el servicio funciona correctamente. Si no está OK reinicia el contenedor.
  • readinessProbe: healthcheck que comprueba que el contenedor se ha levantado correctamente.
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: nginx
  namespace: production
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: nginx
        roles: web
        stages: production
    spec:
      containers:
        - name: nginx
          image: "nginx:latest"
          imagePullPolicy: IfNotPresent
          env:
          - name: TZ
            value: "Europe/Madrid"
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "200m"
              memory: "254Mi"
          ports:
            - name: nginx-port
              containerPort: 80 
          livenessProbe:
            tcpSocket:
              port: nginx-port
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5             
          readinessProbe:
            tcpSocket:
              port: nginx-port 
            initialDelaySeconds: 5
            periodSeconds: 10
          volumeMounts:
            - name: nginx-conf
              mountPath: /etc/nginx/conf.d
            - name: wordpress-files
              mountPath: /var/www/html   
            - name: nginx
              mountPath: /etc/nginx/nginx.conf
              subPath: nginx.conf         
      volumes:
        - name: nginx-conf
          configMap:
            name: nginx-conf
            items:
              - key: virtualhost.conf
                path: virtualhost.conf
              - key: security.conf
                path: security.conf
              - key: cache.conf
                path: cache.conf
        - name: nginx
          configMap:
            name: nginx-conf
            items:
              - key: nginx.conf
                path: nginx.conf
        - name: wordpress-files
          persistentVolumeClaim:
            claimName: wordpress-files

Como se puede ver en el deployment, hay varios volúmenes definidos (esto está abajo del todo en la parte de volumes). En este caso, estamos invocando 2 veces al configmap que hemos definido antes. ¿Por qué? Porque los vamos a montar en paths distintos. Unos van sobre la carpeta conf.d y otro va sobre la raíz de la carpeta de Nginx. Si montamos todos en la raíz de Nginx, se pisarán todos los ficheros que haya. Cuando se quiere montar un solo fichero en un path sin borrar el resto, hay que usar la opción subPath en el volumeMounts.
El tercero, es el volumen que va a tener los ficheros de WordPress. Que estarán compartidos con con el POD de PHP-FPM.
Una vez definidos los volúmenes, hay que montarlos con VolumeMounts.

PHP-FPM

El servicio de PHP-FPM es muy parecido al de Nginx cambiando un par de cosas.

Secrets

A diferencia del servicio de Nginx, el de PHP-FPM tiene secretos. Kubernetes a día de hoy esto no lo tiene muy seguro. Lo secretos pueden ir tanto en base64 como en plano. En futuros posts, miraremos como gestionar esto con Vault. Haremos los 2 ejemplos. En este caso, el YAML se llamará 20-phpfpm-secrets.yaml. En este ejemplo, irán en plano. Y la forma de definirlos es como si fuese una variable.
Para usarlo en texto plano, hay que usar el modo stringData

apiVersion: v1
kind: Secret
metadata:
  name: phpfpm-secrets
  namespace: production
type: Opaque
stringData:
  WORDPRESS_DB_USER: wordpress
  WORDPRESS_DB_PASSWORD: wordpress

Configmap

En este caso, no vamos a definir ficheros en el configmap, si no variables (que no sean sensibles). Hay 2 formas de definirlas, puede ser en el configmap o directamente en el deployment/statefulset. Las ventajas y desventajas son; que el configmap puede ser usado por varios servicios y que es mas limpio, pero cada vez que se cambie algo, además de aplicar los cambios del configmap, hay que reiniciar el POD para que se apliquen los cambios. En el caso de definirlas en el POD, la sintaxis es un poco mas extensa pero cada vez que se cambie una variable, se reiniciará solo el POD.
En este caso el YAML se llamará 21-phpfpm-configmap.yaml.

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: phpfpm-conf
  namespace: production
  labels:
    app: phpfpm
data:
  WORDPRESS_DB_HOST: mysql
  WORDPRESS_DB_NAME: wordpress  

Service

El servicio, en este caso será ClusterIP ya que no es necesario que esté expuesto desde fuera, solo para los servicios del clúster. El fichero es 22-phpfpm-service.yaml.

---
apiVersion: v1
kind: Service
metadata:
  name: phpfpm
  namespace: production
spec:
  selector:
    app: phpfpm
  type: ClusterIP
  ports:
  - name: phpfpm-port
    port: 9000
    targetPort: 9000

Deployment

El deployment en este caso es muy parecido al de Nginx. Por lo que no lo voy a pegar entero. Solo los cachos que sean interesantes. El YAML se llama 23-phpfpm-deployment.yaml.

...
  env:
  - name: TZ
    value: "Europe/Madrid"
  envFrom:
  - configMapRef:
    name: phpfpm-conf
  - secretRef:
    name: phpfpm-secrets
...
  volumeMounts:
  - name: wordpress-files
    mountPath: /var/www/html
volumes:
- name: wordpress-files
  persistentVolumeClaim:
    claimName: wordpress-files

Aquí se puede ver lo que hemos comentado antes de las variables. Las variables, cuando se definen en el deployment, se define con el env y hay que hacer un name y value por cada una, por lo que si hay muchas, el fichero puede ser muy largo. En el caso de que definan en un configmap o un secret, solo hay que invocarlos con el envFrom y se cargarán todas las variables.
Hay otra opción de llamar a variables concretas de un configmap o de un secret. Esta forma también es muy interesante si se quieren coger variables del secret/configmap de otro servicio invocándolas con el nombre que queramos y así no duplicarlas. Sería del siguiente modo:

...
env:
- name: WORDPRESS_DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: phpfpm-secret
        key: WORDPRESS_DB_PASSWORD
...

En el caso de los volúmenes, es lo mismo que en Nginx, en el siguiente YAML definiremos el PVC que se monta.

PersistentVolumeClaim

Este es nuevo, aquí lo que se define es el PVC que vamos a utilizar para guardar los ficheros de WordPress. El YAML se llama 24-phpfpm-pvc.yaml.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-files
  namespace: production
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

MySQL

Y por último, queda definir el servicio de MySQL.

Secrets

En el anterior servicio, hemos creado los secrets en plano. En este caso para cambiar, los vamos a definir en base64 que es la otra forma de hacerlo. Para generar el base64 del secreto que queramos hay que lanzar el siguiente comando:

echo "secreto" | base64

Y si se quiere pasar a texto plano:

echo c2VjcmV0bwo= | base64 -d

El YAML se llama 30-mysql-secrets.yaml. Para añadir los secretos en base64, el modo que hay que usar es data.

apiVersion: v1
kind: Secret
metadata:
  name: mysql-secrets
  namespace: production
type: Opaque
data:
  MYSQL_USER: d29yZHByZXNz
  MYSQL_PASSWORD: d29yZHByZXNz
  MYSQL_ROOT_PASSWORD: Y2hhbmdlbWU=

Configmap

El configmap, es muy parecido al de PHP-FPM. Se llama 31-mysql-configmap.yaml.

Service

Igual que el configmap, este también muy parecido al del PHP-FPM. 32-mysql-service.yaml.

Statefulset

Como hemos comentado antes, el statefulset está pensado para servicios que sean stateful, es decir, que puedan albergar datos que cambien. La declaración de estos, es muy parecida a la del deployment, pero varia en algunas cosas. El YAML se llama 33-mysql-statefulset.yaml.

apiVersion: apps/v1beta2
kind: StatefulSet
...
spec:
  serviceName: mysql
...
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
          subPath: mysql
...
  volumeClaimTemplates:
  - metadata:
      name: mysql-data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

Principalmente, lo que varia es el kind, que es Statefulset, que añade el serviceName y luego que los PVC se puede declarar como volumeClaimTemplates en el mismo YAML. Lo que se hace es declarar un PVC nuevo y luego montarlo en el path que se quiera.

Despliegue

Para desplegar esto, una vez creados todos los archivos y teniendo el Minikube levantado, solo hay que lanzar lo siguiente:

kubectl apply -f .
namespace/production created
configmap/nginx-conf created
service/nginx created
deployment.apps/nginx created
secret/phpfpm-secrets created
configmap/phpfpm-conf created
service/phpfpm created
deployment.apps/phpfpm created
persistentvolumeclaim/wordpress-files created
secret/mysql-secrets created
configmap/mysql-conf created
service/mysql created
statefulset.apps/mysql created

Una vez desplegado, podemos conectarnos al Nginx por el puerto que hemos destinado a ello:

http://IP_Minikube:30036

Para ver la IP de Minikube:

minikube status
host: Running
kubelet: Running
apiserver: Running
kubectl: Correctly Configured: pointing to minikube-vm at 192.168.99.108

Troubleshooting

PODs

Para ver los PODs hay que usar el siguiente comando

kubectl -n production get pods
NAME                      READY   STATUS    RESTARTS   AGE
mysql-0                   1/1     Running   0          85s
nginx-69945748c6-nls7z    1/1     Running   0          86s
phpfpm-697d4f9f9d-pfsdn   1/1     Running   0          86s

Servicios

kubectl -n production get svc
NAME     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
mysql    ClusterIP   10.101.164.170   <none>        3306/TCP       102s
nginx    NodePort    10.109.3.47      <none>        80:30036/TCP   103s
phpfpm   ClusterIP   10.111.192.13    <none>        9000/TCP       103s

PersistentVolumes

kubectl -n production get pv
pvc-4a839582-88fb-11e9-8971-0800271416ea   1Gi        RWX            Delete           Bound    production/wordpress-files      standard                114s
pvc-4a9edb1c-88fb-11e9-8971-0800271416ea   10Gi       RWO            Delete           Bound    production/mysql-data-mysql-0   standard                114s
kubectl -n production get pvc
NAME                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mysql-data-mysql-0   Bound    pvc-4a9edb1c-88fb-11e9-8971-0800271416ea   10Gi       RWO            standard       2m5s
wordpress-files      Bound    pvc-4a839582-88fb-11e9-8971-0800271416ea   1Gi        RWX            standard       2m6s

Secrets

kubectl -n production get secrets
NAME                  TYPE                                  DATA   AGE
default-token-fw6j6   kubernetes.io/service-account-token   3      2m21s
mysql-secrets         Opaque                                3      2m20s
phpfpm-secrets        Opaque                                2      2m21s

Logs

Ver los últimos logs

kubectl -n production logs nginx-69945748c6-nls7z

Ver los logs en tiempo real

kubectl -n production logs -f nginx-69945748c6-nls7z

Ver todos los logs de los contenedores de un POD

kubectl -n production logs -f nginx-69945748c6-nls7z --all-containers=true

Port Forwarding

Traerse un puerto de un POD al local

kubectl -n production port-forward phpfpm-697d4f9f9d-pfsdn 9000:9000

Conectarse al POD

kubectl -n production exec -it nginx-69945748c6-nls7z -- bash

Resumen

En este post hemos visto como empezar a definir un Stack de servicios conectados entre ellos. Utilizando diferentes recursos y tipos propios de Kubernetes. En los siguientes posts intentaré explicar:

  • RBAC y autenticación
  • Ingress Controller
  • Secrets con Vault
  • CNIs
  • Monitorización con Prometheus
  • Gestión de Logs
  • HELM
  • Istio aplicado
  • GitOps
  • Continuous Deployment

3 comments

Leave a Reply

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *